@realitycollective/service-framework-iwsdk 1.0.1-preview.0 → 1.0.1-preview.2

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
@@ -6,10 +6,22 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
6
 
7
7
  ## [1.0.1]
8
8
 
9
- Packaging, tooling and documentation. No runtime behaviour changed, and no public API was added, removed or altered.
9
+ Packaging, tooling and documentation, plus additive runtime API: the engine-free adapter contracts move into the core, the IWSDK adapter derives capabilities from the live session and gains a session facet, and the three.js and Babylon.js packages gain runtime adapters so a three.js, Babylon or desktop app reaches the same seam. Nothing that existed was removed or changed in signature.
10
10
 
11
11
  ### Added
12
12
 
13
+ - `@realitycollective/service-framework` now exports `RuntimeAdapter`, `FrameInfo`, `AdapterCapabilities`, `DEFAULT_CAPABILITIES`, `FrameListener`, `CapabilitiesListener`, `Unsubscribe`, `SnapshotService`, `ServiceContext`, `SnapshotListener` and `MockRuntimeAdapter`. All eleven arrived in `@realitycollective/service-framework-iwsdk`, and none of them ever touched IWSDK: the adapter contract is the seam every host binding needs, `SnapshotService` is a plain `BaseService` subclass, and the mock adapter is how any service is unit-tested headless. An app on three.js or Babylon.js had to depend on the IWSDK package to reach them. They now live in the core, and the IWSDK package re-exports every one of them unchanged, so existing imports keep resolving.
14
+ - `deriveCapabilities(session)` in `@realitycollective/service-framework`, with its structural input types `CapabilitySessionLike` and `CapabilityInputSourceLike`. It reads `immersive`, `handTracking`, `planeDetection` and `passthrough` off a live XR session, and is the derivation `IWSDKAdapter` had been doing privately. A second adapter would have re-implemented the same four rules and then drifted from them, so they live in the core once and every host binding reports the same flags for the same session. `IWSDKAdapter` now calls it and reports exactly what it reported before, and `IWSDKSessionLike` is `CapabilitySessionLike` with `inputSources` required, so nothing changes for a consumer.
15
+ - `WebXRRuntimeAdapter` in `@realitycollective/service-framework-three`, with the structural host types it is written against: `WebXRManagerLike` (`renderer.xr`), `WebXRSystemLike` (`navigator.xr`), `WebXRSessionLike`, `WebXRRuntimeAdapterOptions`, `WebXRManagerEventType`, `WebXRSessionEventType` and `WebXREventListener`. Until now only an IWSDK app could get a `RuntimeAdapter`, so a service written against that seam had no host on a plain three.js page or a desktop build. The adapter orchestrates the entry points the platform already provides - `navigator.xr` to negotiate a session, `renderer.xr` for the session the renderer presents, `setAnimationLoop` for frames - and publishes them through the same interface. It renders nothing, plays nothing and owns no scene state. Given a `host` it owns the animation loop, and given a `scheduler` as well it emits `renderTick` with `source: "three"` exactly as `ThreeRenderLoopBridge` does, so an app needs one loop owner rather than two; given no host, the app drives `emitFrame` itself. `sessionInit` supplies the `XRSessionInit` per mode. Capabilities come from the core's `deriveCapabilities` and are re-derived on the renderer's `sessionstart` and `sessionend` and the session's `inputsourceschange`, with the same override layer, `refreshCapabilities()` and `clearCapabilityOverrides()` the IWSDK adapter has. The same adapter serves a desktop build with no headset: `session.request` returns `{ ok: false, reason: "unsupported" }` where there is no `navigator.xr` or the mode is not supported, and capabilities stay at the all-false defaults.
16
+ - `FIRST_FRAME_DELTA_MS` from the three.js package - the 16 ms the bridge and the adapter both report for a first frame that has no previous timestamp.
17
+ - `BabylonRuntimeAdapter` in `@realitycollective/service-framework-babylon`, with the structural host types it is written against: `BabylonXRExperienceLike` (`WebXRDefaultExperience.baseExperience`), `BabylonSessionManagerLike`, `BabylonXRSessionLike`, `BabylonObservableLike`, `BabylonObserverLike`, `BabylonRuntimeAdapterOptions`, `BabylonXRSessionEventType`, `BabylonXREventListener` and `BabylonWebXRState`, plus the `BABYLON_WEBXR_STATE` and `DEFAULT_REFERENCE_SPACE_TYPE` constants. The package shipped a render-loop bridge and nothing else, so a Babylon app could emit `renderTick` but could not host a service written against `RuntimeAdapter`. The adapter orchestrates the entry points Babylon already provides - the experience helper to negotiate a session, the session manager for the live `XRSession`, `runRenderLoop` for frames - and publishes them through the same interface, with the same members and the same semantics as `WebXRRuntimeAdapter`, so a consumer moving between renderers sees no difference at this seam. Given a `host` it owns the render loop, and given a `scheduler` as well it emits `renderTick` with `source: "babylon"` exactly as `BabylonRenderLoopBridge` does, so an app needs one loop owner rather than two; given no host, the app drives `emitFrame` itself. `referenceSpaceType` defaults to `"local-floor"`, and `sessionInit` supplies the session creation options per mode. Capabilities come from the core's `deriveCapabilities` and are re-derived on `onXRSessionInit`, `onXRSessionEnded` and the session's `inputsourceschange`, with the same override layer, `refreshCapabilities()` and `clearCapabilityOverrides()` the other adapters have. A session started outside the adapter - by Babylon's own enter-XR UI, for instance - is followed through `onStateChangedObservable`, so the facet is correct either way. The same adapter serves a desktop build with no headset: `session.request` returns `{ ok: false, reason: "unsupported" }` where there is no experience or the mode is not supported, and capabilities stay at the all-false defaults. `@babylonjs/core` is still neither imported nor installed: every Babylon shape is structural, and anything a version might move or drop - the session manager, the observables, `isSessionSupportedAsync` - is optional and read through a guard.
18
+ - `FIRST_FRAME_DELTA_MS` from the Babylon.js package - the same 16 ms, now shared by `BabylonRenderLoopBridge` and `BabylonRuntimeAdapter` instead of sitting inline in the bridge.
19
+ - Capability derivation in `IWSDKAdapter`. It reads the live session on construction and on every visibility change: `immersive` when a session exists, `handTracking` from the `hand-tracking` enabled feature or any input source carrying a hand, `planeDetection` from the `plane-detection` enabled feature, and `passthrough` when `environmentBlendMode` is present and is not `opaque`. Subscribers are notified only when a flag actually changes. `setCapabilities` becomes a manual override layer on top, dropped by the new `clearCapabilityOverrides()` or by the new `dispose()`. `refreshCapabilities()` re-derives on demand, for a host whose visibility signal cannot push. Before this, the adapter reported all-false until the app called `setCapabilities` by hand.
20
+ - An optional session facet on `RuntimeAdapter`: `session?: SessionFacet`, with `getState`, `request(mode, options)`, `end()`, `onStateChange` and `onVisibilityChange`, plus the `SessionMode`, `SessionState`, `SessionResult`, `SessionFailureReason`, `SessionVisibility` and `SessionRequestOptions` types and the `DEFAULT_SESSION_TIMEOUT_MS` constant. A request resolves with a result rather than throwing, because a host that cannot start a session is a normal runtime condition. `IWSDKAdapter` implements it over the world's `launchXR` and `exitXR`; `MockRuntimeAdapter` implements it in memory, driven by `simulateSessionStart()`, `simulateSessionEnd()` and `simulateVisibility()`.
21
+ - `RUNTIME_ADAPTER_FACETS`, the list of optional facets an adapter can carry, and the `RuntimeAdapterFacet` type. A conformance test walks the list, so a facet added without a mock implementation fails the suite instead of being found by a consumer.
22
+ - `renderTick` under IWSDK. `makeServiceBridgeSystem` now emits the scheduler's `renderTick` channel on every focused frame, with `source: "iwsdk"`, alongside the adapter's `onFrame` fan-out it already drove. A service written against the scheduler now runs under IWSDK exactly as it does under the three.js and Babylon.js bridges. IWSDK reports its frame delta in seconds and the scheduler's `LifecycleContext` is in milliseconds, so the bridge converts.
23
+ - Structural contracts for the parts of an IWSDK world the adapter now reads: `IWSDKSessionLike`, `IWSDKInputSourceLike`, an optional `session` and `launchXR` / `exitXR` on `IWSDKWorldLike`, and an optional `subscribe` on `IWSDKSignalLike`. Every addition is optional, so a world carrying nothing but the visibility signal still type-checks and still works. `@iwsdk/core` remains undeclared as a dependency of any kind.
24
+ - A shared runtime-adapter conformance suite, run against `MockRuntimeAdapter`, `IWSDKAdapter`, `WebXRRuntimeAdapter` and `BabylonRuntimeAdapter`. It is an in-repo test helper, not a published entry point. Test coverage now measures `@realitycollective/service-framework-iwsdk`, `@realitycollective/service-framework-three` and `@realitycollective/service-framework-babylon` as well, at the same 100% thresholds as the core and client packages.
13
25
  - Updated documentation packs for all projects.
14
26
  - Improved release scripting and validation to improve delivery coherance.
15
27
  - `scripts/release.config.json` - names the core package and the publish order, so the release tooling is identical across every Reality Collective TypeScript repository.
@@ -18,16 +30,21 @@ Packaging, tooling and documentation. No runtime behaviour changed, and no publi
18
30
 
19
31
  ### Changed
20
32
 
33
+ - The runtime adapter now abstracts session lifecycle. It previously did not, on the stated grounds that IWSDK already owns sessions, input and rendering. That held while every consumer was an IWSDK app. It stopped holding when a reference client needed to request a session, end one and read session visibility: with no seam for it, the client reached past the adapter into the host, which is the coupling the adapter exists to prevent. Sessions are now an optional facet, so a host that owns them can expose them and a host that does not can leave the property off. Input and rendering are still not abstracted.
34
+ - `IWSDKAdapter.setCapabilities` now notifies subscribers only when at least one flag actually changes value. It previously notified on every call, including calls that set what was already set. The signature is unchanged; a consumer that relied on a callback per call will see fewer callbacks.
35
+ - `ThreeRenderLoopBridge` moved out of the three.js package's `index.ts` into `three-render-loop-bridge.ts`, and `index.ts` is now a barrel that re-exports it unchanged beside the new adapter. Import paths are unaffected. The file-wide v8 coverage exclusion the package carried went with it: the bridge is tested against a fake host, and the whole package is measured.
21
36
  - CI and deployment merged into one workflow. They previously ran concurrently and repeated the same install, build, typecheck and test on every pull request. The deploy jobs now consume the artifacts the build job already produced.
22
37
  - CI runs on every pull request regardless of target branch, and reports through merge queues.
23
38
  - Published sourcemaps embed their sources (`inlineSources`), so stepping into the framework works for consumers. Declaration maps are no longer emitted, because they can only resolve against a `src` directory that is not shipped. Each package is roughly 12% smaller as a result.
24
39
 
25
40
  ### Fixed
26
41
 
42
+ - `@realitycollective/service-framework-iwsdk` could not be installed alongside `@iwsdk/core` 0.5.x. Its peer range was `>=0.4.0 <0.5.0`, which npm reports as `ERESOLVE` on a clean install and therefore on `npm ci`, regardless of the entry being optional. Consumers needed a local `overrides` block to install at all; that workaround can be deleted once this version is published.
27
43
  - `runtime-examples/*` white-screened with `Cannot read properties of null (reading 'useMemo')`. The Vite aliases point into `../../packages`, so the framework's React binding resolved React from the repository root while the app used its own copy. Two React instances leave the hook dispatcher null. Both example configs now set `resolve.dedupe`.
28
44
 
29
45
  ### Removed
30
46
 
47
+ - The `@iwsdk/core` peer dependency on `@realitycollective/service-framework-iwsdk`, rather than widening it to `<0.6.0`. The bridge never imports `@iwsdk/core`: the primitives it needs (`World`, `createSystem`, `VisibilityState.Visible` and the visibility signal) are typed structurally and passed in by the consumer, and those four contracts are identical across 0.4.x and 0.5.x. A range that constrains nothing while still being able to fail a consumer's install is a liability, so there is now no entry of any kind. The package is verified against IWSDK 0.4.x and 0.5.x, and its example remains engine-free because it mocks those primitives.
31
48
  - Build output is no longer committed: 72 files under `packages/*/dist/` and 44 compiled `.js`, `.d.ts` and `.map` files that sat beside the sources in `packages/*/src/`. The test suite had been executing that committed JavaScript rather than the TypeScript, so coverage now measures `src/**/*.ts`.
32
49
  - The generated `coverage/` report, which every test run rewrote with a new timestamp.
33
50
 
package/README.md CHANGED
@@ -24,21 +24,25 @@ ServiceBridgeSystem ← the only place the engine touches services
24
24
  every update(delta, time):
25
25
  • visibilityState → manager.emitFocusChange / emitPauseChange
26
26
  • if focused: adapter.emitFrame(time, delta)
27
+ • if focused: scheduler.emit("renderTick", { source: "iwsdk", ... })
27
28
 
28
- IWSDKAdapter (RuntimeAdapter) onFrame fan-out services
29
+ IWSDKAdapter (RuntimeAdapter) onFrame fan-out services
30
+ ServiceManager.scheduler → renderTick channel → services
29
31
 
30
32
  ServiceManager → your SnapshotService graph
31
33
  ```
32
34
 
35
+ Each focused frame goes out twice, on the two channels a service can be written against. `adapter.onFrame` is the adapter seam, and carries IWSDK's own units: `timestamp` in milliseconds, `delta` in seconds. `renderTick` is the scheduler channel the three.js and Babylon.js bridges emit, and carries a `LifecycleContext` in scheduler units: `timestamp` and `deltaTime` both in milliseconds, a frame counter, and `source: "iwsdk"`. A service written against `renderTick` therefore runs under IWSDK exactly as it does under three.js, with no change. Frames while the session is not focused are emitted on neither channel and do not advance the counter.
36
+
33
37
  **Invariant:** services depend only on `RuntimeAdapter`, never on `@iwsdk/core`. That is what keeps them unit-testable headless - swap `IWSDKAdapter` for `MockRuntimeAdapter`.
34
38
 
35
39
  ---
36
40
 
37
41
  ## No hard dependency on `@iwsdk/core`
38
42
 
39
- Like the three.js and Babylon.js bridges keep their renderer packages at arm's length, this package **never imports `@iwsdk/core`**. The IWSDK primitives the bridge needs - `createSystem` and the `VisibilityState.Visible` value - are passed in by the consumer (who owns IWSDK). This keeps the package tree-shakeable, version-tolerant across IWSDK `0.4.x`, and trivially mockable in unit tests.
43
+ Like the three.js and Babylon.js bridges keep their renderer packages at arm's length, this package **never imports `@iwsdk/core`**. The IWSDK primitives the bridge needs - `createSystem` and the `VisibilityState.Visible` value - are passed in by the consumer (who owns IWSDK). This keeps the package tree-shakeable, version-tolerant across IWSDK `0.4.x` and `0.5.x`, and trivially mockable in unit tests.
40
44
 
41
- `@iwsdk/core` is declared as an **optional peer dependency**.
45
+ `@iwsdk/core` is **not declared as a dependency of any kind** - not even an optional peer. Everything this package reads off the world is structurally typed in `iwsdk-host.ts` and passed in: `createSystem`, the `VisibilityState.Visible` value, the visibility signal, and - all optional - `visibilityState.subscribe`, the live `session`, `launchXR` and `exitXR`. Because the additions are optional, a world that carries nothing but the visibility signal still type-checks and still works; it simply reports no capabilities and no session. Those contracts are identical across 0.4.x and 0.5.x. A peer range here would constrain nothing while still being able to fail a consumer's clean install, so there is deliberately no entry.
42
46
 
43
47
  ---
44
48
 
@@ -77,10 +81,10 @@ Game logic ticks **only while the session is visible/focused** - in the browser
77
81
 
78
82
  ## Services own their state: `SnapshotService`
79
83
 
80
- `SnapshotService<TConfig, TSnapshot>` is the "services own state" base - one immutable snapshot plus pub/sub. Subscribers receive the current value immediately, then every publish. It has **no IWSDK dependency**.
84
+ `SnapshotService<TConfig, TSnapshot>` is the "services own state" base - one immutable snapshot plus pub/sub. Subscribers receive the current value immediately, then every publish. It has **no IWSDK dependency**, which is why it now lives in the core package; this package re-exports it, so the older import still works.
81
85
 
82
86
  ```typescript
83
- import { SnapshotService, type ServiceContext, type RuntimeAdapter } from "@realitycollective/service-framework-iwsdk";
87
+ import { SnapshotService, type ServiceContext, type RuntimeAdapter } from "@realitycollective/service-framework";
84
88
 
85
89
  interface EnergySnapshot { readonly energy: number; }
86
90
 
@@ -111,7 +115,7 @@ const unsubscribe = energy.subscribe(({ energy }) => hud.setEnergy(energy));
111
115
  Services run against `MockRuntimeAdapter` with no IWSDK / renderer / headset. Tests drive the loop by calling `emitFrame`:
112
116
 
113
117
  ```typescript
114
- import { MockRuntimeAdapter } from "@realitycollective/service-framework-iwsdk";
118
+ import { MockRuntimeAdapter } from "@realitycollective/service-framework";
115
119
 
116
120
  const adapter = new MockRuntimeAdapter({ immersive: true });
117
121
  const service = new EnergyService(makeContext(), adapter);
@@ -123,32 +127,90 @@ expect(service.getSnapshot().energy).toBeLessThan(1);
123
127
 
124
128
  No `@iwsdk/core` import appears anywhere in the test.
125
129
 
130
+ `MockRuntimeAdapter` implements the session facet in memory too: `simulateSessionStart()`, `simulateSessionEnd()` and `simulateVisibility(v)` drive it, and `request()` resolves `{ ok: true }` when a start is simulated before the timeout, `{ ok: false, reason: "timeout" }` otherwise. Both adapters run the same in-repo conformance suite, so a service that behaves one way headless behaves the same way on a headset.
131
+
126
132
  ---
127
133
 
128
134
  ## Capabilities
129
135
 
130
- `AdapterCapabilities` (`immersive`, `handTracking`, `planeDetection`, `passthrough`) is what gating services read (`adapter.getCapabilities()`) or subscribe to (`adapter.onCapabilitiesChange(cb)` - mirrors `onFrame`, so gates don't poll every frame). Refine them with `adapter.setCapabilities({ ... })`.
136
+ `AdapterCapabilities` (`immersive`, `handTracking`, `planeDetection`, `passthrough`) is what gating services read (`adapter.getCapabilities()`) or subscribe to (`adapter.onCapabilitiesChange(cb)` - mirrors `onFrame`, so gates don't poll every frame).
137
+
138
+ `IWSDKAdapter` derives all four from the live session through `deriveCapabilities(session)`, exported by `@realitycollective/service-framework`. The rules live in the core so that every host binding - this one, the three.js `WebXRRuntimeAdapter`, and whatever comes next - reports the same flags for the same session. It derives on construction, and again every time the world's visibility signal fires, which is when a session comes or goes:
139
+
140
+ | Flag | Derived from |
141
+ | --- | --- |
142
+ | `immersive` | a session exists on the world |
143
+ | `handTracking` | `"hand-tracking"` in `session.enabledFeatures`, or any `session.inputSources[i].hand` |
144
+ | `planeDetection` | `"plane-detection"` in `session.enabledFeatures` |
145
+ | `passthrough` | `session.environmentBlendMode` is present and is not `"opaque"` |
146
+
147
+ With no session the adapter reports `DEFAULT_CAPABILITIES`, all false, so gating services behave conservatively before the player enters XR. Subscribers are notified only when a flag actually changes, so a signal that fires every visibility blip does not wake every gate.
148
+
149
+ Two hosts need a nudge. If the world's `visibilityState` has no `subscribe`, nothing tells the adapter to re-derive: call `adapter.refreshCapabilities()` after the host changes something. Call it too if the host enables a feature mid-session without a visibility transition.
150
+
151
+ ### Overrides
152
+
153
+ `adapter.setCapabilities({ ... })` still exists, and is now a layer on top of the derived values rather than the only source of them. An override wins for as long as it is set: it survives every later derivation, so forcing `passthrough: true` on a device that under-reports its blend mode keeps working when the session changes. Overrides are dropped by `adapter.clearCapabilityOverrides()`, which falls back to the derived values, or by `adapter.dispose()`.
154
+
155
+ `adapter.dispose()` releases the visibility subscription, settles any in-flight session request, drops the overrides and clears every listener. Call it when tearing the world down.
156
+
157
+ ---
158
+
159
+ ## Sessions
160
+
161
+ `adapter.session` is the optional session facet from `RuntimeAdapter`, implemented here over the world's `launchXR` and `exitXR`. It exists so a client can start, end and observe an XR session without reaching past the adapter into IWSDK - which is what a client had to do before, and the reason the "the adapter does not abstract sessions" position was reversed.
162
+
163
+ ```typescript
164
+ const result = await adapter.session.request("immersive-vr", { timeoutMs: 8000 });
165
+
166
+ if (!result.ok) {
167
+ // "unsupported" | "denied" | "timeout" | "error" - a normal runtime outcome,
168
+ // so it comes back as a result rather than a thrown error.
169
+ showEnterVrError(result.reason);
170
+ }
171
+
172
+ const stop = adapter.session.onStateChange((state) => hud.setSessionState(state));
173
+ adapter.session.onVisibilityChange((visibility) => hud.setDimmed(visibility !== "visible"));
174
+
175
+ await adapter.session.end();
176
+ ```
177
+
178
+ - `getState()` walks `"none"` → `"requesting"` → `"active"` → `"ending"` → `"none"`.
179
+ - `request(mode, options)` calls `launchXR` with `{ sessionMode: mode }` (the `XROptions` shape IWSDK accepts; its `SessionMode` enum values are the WebXR mode strings) and resolves once a session appears, whether the adapter learns that from the visibility signal or from its polling fallback. A synchronous throw from `launchXR` comes back as `{ ok: false, reason: "unsupported", error }`. Nothing appearing within `timeoutMs` (default 10000) comes back as `{ ok: false, reason: "timeout" }`.
180
+ - `end()` calls `exitXR` and resolves once the session is gone.
181
+ - `onVisibilityChange` maps IWSDK's signal onto `"visible"`, `"visible-blurred"`, `"hidden"` and `"non-immersive"`. A value the adapter does not recognise is reported as `"hidden"`, because treating an unknown state as visible would keep game logic running when it should not.
131
182
 
132
- > **One gap still to close:** deriving capabilities from the live `XRSession` (enabled features, blend mode, hand input sources) is intentionally left to the host wiring because it needs IWSDK session internals. Until that is wired, the adapter reports `DEFAULT_CAPABILITIES` (all-false) and gating services behave conservatively. `IWSDKAdapter.getWorld()` exposes the bound world as the source for that derivation, and `onCapabilitiesChange` is the notification channel for when it lands.
183
+ A world with no `launchXR` reports `{ ok: false, reason: "unsupported" }` rather than throwing, so a 2D preview build needs no special case.
133
184
 
134
185
  ---
135
186
 
136
187
  ## API surface
137
188
 
189
+ Owned by this package:
190
+
138
191
  | Symbol | Kind | Purpose |
139
192
  | --- | --- | --- |
140
- | `RuntimeAdapter` | interface | The seam services depend on (`onFrame`, `getCapabilities`, `onCapabilitiesChange`). |
193
+ | `IWSDKAdapter` | class | Production adapter; `emitFrame`, `refreshCapabilities`, `setCapabilities`, `clearCapabilityOverrides`, `session`, `getWorld`, `dispose`. |
194
+ | `makeServiceBridgeSystem` | factory | Returns the IWSDK `ServiceBridgeSystem` class; pumps `onFrame` and `renderTick`. |
195
+ | `ServiceBridgeSystemOptions` | interface | `{ adapter, manager, world, createSystem, visibleState }`. |
196
+ | `startServiceRuntime` / `ServiceRuntime` | function / interface | Bootstraps `{ manager, adapter }` from a profile factory. |
197
+ | `IWSDKWorldLike` / `IWSDKSignalLike` / `IWSDKSessionLike` / `IWSDKInputSourceLike` / `IWSDKSystemLike` / `IWSDKSystemConstructor` / `CreateSystemLike` | types | Structural `@iwsdk/core` contracts (no engine import). |
198
+
199
+ Re-exported from `@realitycollective/service-framework`. None of these ever touched IWSDK, and every host binding needs them, so they moved into the core in 1.0.1. They are re-exported here unchanged, so importing them from this package keeps working; new code should import them from the core package.
200
+
201
+ | Symbol | Kind | Purpose |
202
+ | --- | --- | --- |
203
+ | `RuntimeAdapter` | interface | The seam services depend on (`onFrame`, `getCapabilities`, `onCapabilitiesChange`, optional `session`). |
141
204
  | `FrameInfo` | interface | `{ timestamp, delta }`. |
142
205
  | `AdapterCapabilities` / `DEFAULT_CAPABILITIES` | interface / const | XR capability flags; all-false default. |
143
206
  | `Unsubscribe` / `FrameListener` / `CapabilitiesListener` | types | Callback / handle aliases. |
144
- | `IWSDKAdapter` | class | Production adapter; `emitFrame`, `setCapabilities`, `getWorld`. |
145
- | `MockRuntimeAdapter` | class | Headless adapter; `emitFrame(ts?, delta?)`, `setCapabilities`. |
207
+ | `SessionFacet` | interface | `getState`, `request`, `end`, `onStateChange`, `onVisibilityChange`. |
208
+ | `SessionMode` / `SessionState` / `SessionResult` / `SessionFailureReason` / `SessionVisibility` / `SessionRequestOptions` | types | The session facet's vocabulary. |
209
+ | `DEFAULT_SESSION_TIMEOUT_MS` | const | 10000 - the default `request` timeout. |
210
+ | `RUNTIME_ADAPTER_FACETS` / `RuntimeAdapterFacet` | const / type | Every optional facet an adapter can carry; walked by the conformance suite. |
211
+ | `MockRuntimeAdapter` | class | Headless adapter; `emitFrame(ts?, delta?)`, `setCapabilities`, `simulateSessionStart`, `simulateSessionEnd`, `simulateVisibility`. |
146
212
  | `SnapshotService<C, S>` | abstract class | State-owning base (`subscribe` / `getSnapshot` / `publishSnapshot` / `updateSnapshot`). |
147
213
  | `ServiceContext<C>` / `SnapshotListener<S>` | types | Activation-context alias; snapshot callback. |
148
- | `makeServiceBridgeSystem` | factory | Returns the IWSDK `ServiceBridgeSystem` class. |
149
- | `ServiceBridgeSystemOptions` | interface | `{ adapter, manager, world, createSystem, visibleState }`. |
150
- | `startServiceRuntime` / `ServiceRuntime` | function / interface | Bootstraps `{ manager, adapter }` from a profile factory. |
151
- | `IWSDKWorldLike` / `CreateSystemLike` / … | types | Structural `@iwsdk/core` contracts (no engine import). |
152
214
 
153
215
  ---
154
216
 
package/dist/index.d.ts CHANGED
@@ -1,10 +1,13 @@
1
- export { DEFAULT_CAPABILITIES } from "./runtime-adapter.js";
2
- export type { AdapterCapabilities, CapabilitiesListener, FrameInfo, FrameListener, RuntimeAdapter, Unsubscribe, } from "./runtime-adapter.js";
3
- export type { CreateSystemLike, IWSDKSignalLike, IWSDKSystemConstructor, IWSDKSystemLike, IWSDKWorldLike, } from "./iwsdk-host.js";
1
+ /**
2
+ * The runtime-adapter contract, the snapshot service base and the headless mock
3
+ * moved into `@realitycollective/service-framework` in 1.0.1: none of them ever
4
+ * touched IWSDK, and every host binding needs them. They are re-exported here
5
+ * unchanged so existing imports from this package keep working.
6
+ */
7
+ export { DEFAULT_CAPABILITIES, DEFAULT_SESSION_TIMEOUT_MS, MockRuntimeAdapter, RUNTIME_ADAPTER_FACETS, SnapshotService, } from "@realitycollective/service-framework";
8
+ export type { AdapterCapabilities, CapabilitiesListener, FrameInfo, FrameListener, RuntimeAdapter, RuntimeAdapterFacet, ServiceContext, SessionFacet, SessionFailureReason, SessionMode, SessionRequestOptions, SessionResult, SessionState, SessionVisibility, SnapshotListener, Unsubscribe, } from "@realitycollective/service-framework";
9
+ export type { CreateSystemLike, IWSDKInputSourceLike, IWSDKSessionLike, IWSDKSignalLike, IWSDKSystemConstructor, IWSDKSystemLike, IWSDKWorldLike, } from "./iwsdk-host.js";
4
10
  export { IWSDKAdapter } from "./iwsdk-adapter.js";
5
- export { MockRuntimeAdapter } from "./mock-runtime-adapter.js";
6
- export { SnapshotService } from "./snapshot-service.js";
7
- export type { ServiceContext, SnapshotListener } from "./snapshot-service.js";
8
11
  export { makeServiceBridgeSystem } from "./service-bridge-system.js";
9
12
  export type { ServiceBridgeSystemOptions } from "./service-bridge-system.js";
10
13
  export { startServiceRuntime } from "./bootstrap.js";
package/dist/index.js CHANGED
@@ -1,7 +1,11 @@
1
- export { DEFAULT_CAPABILITIES } from "./runtime-adapter.js";
1
+ /**
2
+ * The runtime-adapter contract, the snapshot service base and the headless mock
3
+ * moved into `@realitycollective/service-framework` in 1.0.1: none of them ever
4
+ * touched IWSDK, and every host binding needs them. They are re-exported here
5
+ * unchanged so existing imports from this package keep working.
6
+ */
7
+ export { DEFAULT_CAPABILITIES, DEFAULT_SESSION_TIMEOUT_MS, MockRuntimeAdapter, RUNTIME_ADAPTER_FACETS, SnapshotService, } from "@realitycollective/service-framework";
2
8
  export { IWSDKAdapter } from "./iwsdk-adapter.js";
3
- export { MockRuntimeAdapter } from "./mock-runtime-adapter.js";
4
- export { SnapshotService } from "./snapshot-service.js";
5
9
  export { makeServiceBridgeSystem } from "./service-bridge-system.js";
6
10
  export { startServiceRuntime } from "./bootstrap.js";
7
11
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAkB5D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["export { DEFAULT_CAPABILITIES } from \"./runtime-adapter.js\";\nexport type {\n AdapterCapabilities,\n CapabilitiesListener,\n FrameInfo,\n FrameListener,\n RuntimeAdapter,\n Unsubscribe,\n} from \"./runtime-adapter.js\";\n\nexport type {\n CreateSystemLike,\n IWSDKSignalLike,\n IWSDKSystemConstructor,\n IWSDKSystemLike,\n IWSDKWorldLike,\n} from \"./iwsdk-host.js\";\n\nexport { IWSDKAdapter } from \"./iwsdk-adapter.js\";\nexport { MockRuntimeAdapter } from \"./mock-runtime-adapter.js\";\n\nexport { SnapshotService } from \"./snapshot-service.js\";\nexport type { ServiceContext, SnapshotListener } from \"./snapshot-service.js\";\n\nexport { makeServiceBridgeSystem } from \"./service-bridge-system.js\";\nexport type { ServiceBridgeSystemOptions } from \"./service-bridge-system.js\";\n\nexport { startServiceRuntime } from \"./bootstrap.js\";\nexport type { ServiceRuntime } from \"./bootstrap.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,kBAAkB,EAClB,sBAAsB,EACtB,eAAe,GAChB,MAAM,sCAAsC,CAAC;AA8B9C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["/**\n * The runtime-adapter contract, the snapshot service base and the headless mock\n * moved into `@realitycollective/service-framework` in 1.0.1: none of them ever\n * touched IWSDK, and every host binding needs them. They are re-exported here\n * unchanged so existing imports from this package keep working.\n */\nexport {\n DEFAULT_CAPABILITIES,\n DEFAULT_SESSION_TIMEOUT_MS,\n MockRuntimeAdapter,\n RUNTIME_ADAPTER_FACETS,\n SnapshotService,\n} from \"@realitycollective/service-framework\";\nexport type {\n AdapterCapabilities,\n CapabilitiesListener,\n FrameInfo,\n FrameListener,\n RuntimeAdapter,\n RuntimeAdapterFacet,\n ServiceContext,\n SessionFacet,\n SessionFailureReason,\n SessionMode,\n SessionRequestOptions,\n SessionResult,\n SessionState,\n SessionVisibility,\n SnapshotListener,\n Unsubscribe,\n} from \"@realitycollective/service-framework\";\n\nexport type {\n CreateSystemLike,\n IWSDKInputSourceLike,\n IWSDKSessionLike,\n IWSDKSignalLike,\n IWSDKSystemConstructor,\n IWSDKSystemLike,\n IWSDKWorldLike,\n} from \"./iwsdk-host.js\";\n\nexport { IWSDKAdapter } from \"./iwsdk-adapter.js\";\n\nexport { makeServiceBridgeSystem } from \"./service-bridge-system.js\";\nexport type { ServiceBridgeSystemOptions } from \"./service-bridge-system.js\";\n\nexport { startServiceRuntime } from \"./bootstrap.js\";\nexport type { ServiceRuntime } from \"./bootstrap.js\";\n"]}
@@ -1,29 +1,80 @@
1
1
  /**
2
2
  * IWSDK implementation of {@link RuntimeAdapter}. IWSDK owns the render loop, so
3
- * the adapter is a passive fan-out: the `ServiceBridgeSystem` (a normal IWSDK
4
- * system) calls {@link IWSDKAdapter.emitFrame} once per frame and the adapter
5
- * notifies subscribed services. Capabilities are refined from the XR session via
6
- * {@link IWSDKAdapter.setCapabilities}.
3
+ * frames are pushed in rather than pulled: the `ServiceBridgeSystem` (a normal
4
+ * IWSDK system) calls {@link IWSDKAdapter.emitFrame} once per frame and the
5
+ * adapter notifies subscribed services.
6
+ *
7
+ * Capabilities are derived from the live session on construction and on every
8
+ * visibility change, through the core's shared `deriveCapabilities`, with a
9
+ * manual override layer on top. Session lifecycle is
10
+ * exposed through {@link IWSDKAdapter.session}, over the world's `launchXR` and
11
+ * `exitXR` entry points.
7
12
  */
8
- import { type AdapterCapabilities, type CapabilitiesListener, type FrameListener, type RuntimeAdapter, type Unsubscribe } from "./runtime-adapter.js";
13
+ import { type AdapterCapabilities, type CapabilitiesListener, type FrameListener, type RuntimeAdapter, type SessionFacet, type Unsubscribe } from "@realitycollective/service-framework";
9
14
  import type { IWSDKWorldLike } from "./iwsdk-host.js";
10
15
  export declare class IWSDKAdapter implements RuntimeAdapter {
11
16
  private readonly world;
12
17
  private readonly frameListeners;
13
18
  private readonly capabilitiesListeners;
19
+ private readonly stateListeners;
20
+ private readonly visibilityListeners;
21
+ private readonly pendingWaits;
22
+ private derived;
23
+ private overrides;
14
24
  private capabilities;
25
+ private sessionState;
26
+ private unsubscribeVisibility;
27
+ /** Session lifecycle over the world's `launchXR` / `exitXR` entry points. */
28
+ readonly session: SessionFacet;
15
29
  constructor(world: IWSDKWorldLike);
16
30
  onFrame(listener: FrameListener): Unsubscribe;
17
31
  getCapabilities(): AdapterCapabilities;
18
32
  onCapabilitiesChange(listener: CapabilitiesListener): Unsubscribe;
33
+ /** The IWSDK `World` this adapter is bound to. */
34
+ getWorld(): IWSDKWorldLike;
19
35
  /**
20
- * The IWSDK `World` this adapter is bound to. Reserved for capability
21
- * derivation from the live XR session (see the package README "Capabilities"
22
- * section) - the one piece still to be wired to the real IWSDK session API.
36
+ * Re-read the live session and publish any capability change. The adapter
37
+ * does this itself whenever the visibility signal fires; call it directly on
38
+ * a host whose signal has no `subscribe`, or after the host enables a feature
39
+ * mid-session.
40
+ */
41
+ refreshCapabilities(): void;
42
+ /**
43
+ * Force capability flags regardless of what the session reports. Overrides
44
+ * are a layer on top of the derived values: they win for as long as they are
45
+ * set, survive every later derivation, and are dropped only by
46
+ * {@link IWSDKAdapter.clearCapabilityOverrides} or
47
+ * {@link IWSDKAdapter.dispose}. Subscribers are notified if the effective
48
+ * capabilities changed.
23
49
  */
24
- getWorld(): IWSDKWorldLike;
25
- /** Refine capabilities once the XR session reports them; notifies subscribers. */
26
50
  setCapabilities(capabilities: Partial<AdapterCapabilities>): void;
51
+ /** Drop every manual override and fall back to the derived capabilities. */
52
+ clearCapabilityOverrides(): void;
27
53
  /** Called once per frame by the ECS bridge system. */
28
54
  emitFrame(timestamp: number, delta: number): void;
55
+ /**
56
+ * Release everything the adapter holds: the visibility subscription, any
57
+ * in-flight session wait, the manual overrides and every listener.
58
+ */
59
+ dispose(): void;
60
+ private hasSession;
61
+ private publishCapabilities;
62
+ private handleVisibilityChange;
63
+ /**
64
+ * Follow the world when nothing local is mid-transition. `request()` and
65
+ * `end()` own the state while they run, so the signal does not fight them.
66
+ */
67
+ private syncSessionState;
68
+ private requestSession;
69
+ private endSession;
70
+ /**
71
+ * Resolve `true` once `predicate` holds, `false` on timeout. The visibility
72
+ * signal drives this where the host has one; the interval is the fallback for
73
+ * a host whose signal does not push, and for a session that appears without
74
+ * a visibility transition.
75
+ */
76
+ private waitFor;
77
+ private checkWaits;
78
+ private settleWait;
79
+ private setSessionState;
29
80
  }
@@ -1,18 +1,73 @@
1
1
  /**
2
2
  * IWSDK implementation of {@link RuntimeAdapter}. IWSDK owns the render loop, so
3
- * the adapter is a passive fan-out: the `ServiceBridgeSystem` (a normal IWSDK
4
- * system) calls {@link IWSDKAdapter.emitFrame} once per frame and the adapter
5
- * notifies subscribed services. Capabilities are refined from the XR session via
6
- * {@link IWSDKAdapter.setCapabilities}.
3
+ * frames are pushed in rather than pulled: the `ServiceBridgeSystem` (a normal
4
+ * IWSDK system) calls {@link IWSDKAdapter.emitFrame} once per frame and the
5
+ * adapter notifies subscribed services.
6
+ *
7
+ * Capabilities are derived from the live session on construction and on every
8
+ * visibility change, through the core's shared `deriveCapabilities`, with a
9
+ * manual override layer on top. Session lifecycle is
10
+ * exposed through {@link IWSDKAdapter.session}, over the world's `launchXR` and
11
+ * `exitXR` entry points.
7
12
  */
8
- import { DEFAULT_CAPABILITIES, } from "./runtime-adapter.js";
13
+ import { DEFAULT_CAPABILITIES, DEFAULT_SESSION_TIMEOUT_MS, deriveCapabilities, } from "@realitycollective/service-framework";
14
+ /** How often the session waits re-check the world when the signal is silent. */
15
+ const SESSION_POLL_INTERVAL_MS = 50;
16
+ const SESSION_VISIBILITY_VALUES = new Set([
17
+ "visible",
18
+ "visible-blurred",
19
+ "hidden",
20
+ "non-immersive",
21
+ ]);
22
+ /**
23
+ * IWSDK's visibility signal carries the WebXR visibility strings. Anything this
24
+ * adapter does not recognise is reported as `"hidden"`, because treating an
25
+ * unknown state as visible would keep game logic running when it should not.
26
+ */
27
+ function toSessionVisibility(value) {
28
+ if (typeof value === "string" && SESSION_VISIBILITY_VALUES.has(value)) {
29
+ return value;
30
+ }
31
+ return "hidden";
32
+ }
9
33
  export class IWSDKAdapter {
10
34
  world;
11
35
  frameListeners = new Set();
12
36
  capabilitiesListeners = new Set();
37
+ stateListeners = new Set();
38
+ visibilityListeners = new Set();
39
+ pendingWaits = new Set();
40
+ derived = DEFAULT_CAPABILITIES;
41
+ overrides = {};
13
42
  capabilities = DEFAULT_CAPABILITIES;
43
+ sessionState = "none";
44
+ unsubscribeVisibility;
45
+ /** Session lifecycle over the world's `launchXR` / `exitXR` entry points. */
46
+ session = {
47
+ getState: () => this.sessionState,
48
+ request: (mode, options) => this.requestSession(mode, options),
49
+ end: () => this.endSession(),
50
+ onStateChange: (listener) => {
51
+ this.stateListeners.add(listener);
52
+ return () => {
53
+ this.stateListeners.delete(listener);
54
+ };
55
+ },
56
+ onVisibilityChange: (listener) => {
57
+ this.visibilityListeners.add(listener);
58
+ return () => {
59
+ this.visibilityListeners.delete(listener);
60
+ };
61
+ },
62
+ };
14
63
  constructor(world) {
15
64
  this.world = world;
65
+ this.unsubscribeVisibility = world.visibilityState.subscribe?.((value) => {
66
+ this.handleVisibilityChange(value);
67
+ });
68
+ this.derived = deriveCapabilities(this.world.session);
69
+ this.capabilities = this.derived;
70
+ this.sessionState = this.hasSession() ? "active" : "none";
16
71
  }
17
72
  onFrame(listener) {
18
73
  this.frameListeners.add(listener);
@@ -29,23 +84,170 @@ export class IWSDKAdapter {
29
84
  this.capabilitiesListeners.delete(listener);
30
85
  };
31
86
  }
32
- /**
33
- * The IWSDK `World` this adapter is bound to. Reserved for capability
34
- * derivation from the live XR session (see the package README "Capabilities"
35
- * section) - the one piece still to be wired to the real IWSDK session API.
36
- */
87
+ /** The IWSDK `World` this adapter is bound to. */
37
88
  getWorld() {
38
89
  return this.world;
39
90
  }
40
- /** Refine capabilities once the XR session reports them; notifies subscribers. */
91
+ /**
92
+ * Re-read the live session and publish any capability change. The adapter
93
+ * does this itself whenever the visibility signal fires; call it directly on
94
+ * a host whose signal has no `subscribe`, or after the host enables a feature
95
+ * mid-session.
96
+ */
97
+ refreshCapabilities() {
98
+ this.derived = deriveCapabilities(this.world.session);
99
+ this.publishCapabilities();
100
+ }
101
+ /**
102
+ * Force capability flags regardless of what the session reports. Overrides
103
+ * are a layer on top of the derived values: they win for as long as they are
104
+ * set, survive every later derivation, and are dropped only by
105
+ * {@link IWSDKAdapter.clearCapabilityOverrides} or
106
+ * {@link IWSDKAdapter.dispose}. Subscribers are notified if the effective
107
+ * capabilities changed.
108
+ */
41
109
  setCapabilities(capabilities) {
42
- this.capabilities = { ...this.capabilities, ...capabilities };
43
- this.capabilitiesListeners.forEach((listener) => listener(this.capabilities));
110
+ this.overrides = { ...this.overrides, ...capabilities };
111
+ this.publishCapabilities();
112
+ }
113
+ /** Drop every manual override and fall back to the derived capabilities. */
114
+ clearCapabilityOverrides() {
115
+ this.overrides = {};
116
+ this.publishCapabilities();
44
117
  }
45
118
  /** Called once per frame by the ECS bridge system. */
46
119
  emitFrame(timestamp, delta) {
47
120
  const frame = { timestamp, delta };
48
121
  this.frameListeners.forEach((listener) => listener(frame));
49
122
  }
123
+ /**
124
+ * Release everything the adapter holds: the visibility subscription, any
125
+ * in-flight session wait, the manual overrides and every listener.
126
+ */
127
+ dispose() {
128
+ this.unsubscribeVisibility?.();
129
+ this.unsubscribeVisibility = undefined;
130
+ for (const wait of Array.from(this.pendingWaits)) {
131
+ this.settleWait(wait, false);
132
+ }
133
+ this.overrides = {};
134
+ this.frameListeners.clear();
135
+ this.capabilitiesListeners.clear();
136
+ this.stateListeners.clear();
137
+ this.visibilityListeners.clear();
138
+ }
139
+ hasSession() {
140
+ return Boolean(this.world.session);
141
+ }
142
+ publishCapabilities() {
143
+ const next = { ...this.derived, ...this.overrides };
144
+ const current = this.capabilities;
145
+ if (next.immersive === current.immersive &&
146
+ next.handTracking === current.handTracking &&
147
+ next.planeDetection === current.planeDetection &&
148
+ next.passthrough === current.passthrough) {
149
+ return;
150
+ }
151
+ this.capabilities = next;
152
+ this.capabilitiesListeners.forEach((listener) => listener(next));
153
+ }
154
+ handleVisibilityChange(value) {
155
+ this.refreshCapabilities();
156
+ this.syncSessionState();
157
+ this.checkWaits();
158
+ const visibility = toSessionVisibility(value);
159
+ this.visibilityListeners.forEach((listener) => listener(visibility));
160
+ }
161
+ /**
162
+ * Follow the world when nothing local is mid-transition. `request()` and
163
+ * `end()` own the state while they run, so the signal does not fight them.
164
+ */
165
+ syncSessionState() {
166
+ if (this.sessionState === "requesting" || this.sessionState === "ending") {
167
+ return;
168
+ }
169
+ this.setSessionState(this.hasSession() ? "active" : "none");
170
+ }
171
+ async requestSession(mode, options) {
172
+ if (this.sessionState === "active") {
173
+ return { ok: true };
174
+ }
175
+ const launch = this.world.launchXR;
176
+ if (!launch) {
177
+ return { ok: false, reason: "unsupported" };
178
+ }
179
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
180
+ this.setSessionState("requesting");
181
+ try {
182
+ // IWSDK reads `XROptions.sessionMode`; its `SessionMode` enum values are the
183
+ // WebXR mode strings, so the facet mode passes through unchanged.
184
+ launch.call(this.world, { sessionMode: mode });
185
+ }
186
+ catch (error) {
187
+ this.setSessionState("none");
188
+ return { ok: false, reason: "unsupported", error };
189
+ }
190
+ const started = await this.waitFor(() => this.hasSession(), timeoutMs);
191
+ if (!started) {
192
+ this.setSessionState("none");
193
+ return { ok: false, reason: "timeout" };
194
+ }
195
+ this.setSessionState("active");
196
+ this.refreshCapabilities();
197
+ return { ok: true };
198
+ }
199
+ async endSession() {
200
+ if (this.sessionState === "none") {
201
+ return;
202
+ }
203
+ const exit = this.world.exitXR;
204
+ this.setSessionState("ending");
205
+ if (exit) {
206
+ exit.call(this.world);
207
+ }
208
+ await this.waitFor(() => !this.hasSession(), DEFAULT_SESSION_TIMEOUT_MS);
209
+ this.setSessionState("none");
210
+ this.refreshCapabilities();
211
+ }
212
+ /**
213
+ * Resolve `true` once `predicate` holds, `false` on timeout. The visibility
214
+ * signal drives this where the host has one; the interval is the fallback for
215
+ * a host whose signal does not push, and for a session that appears without
216
+ * a visibility transition.
217
+ */
218
+ waitFor(predicate, timeoutMs) {
219
+ if (predicate()) {
220
+ return Promise.resolve(true);
221
+ }
222
+ return new Promise((resolve) => {
223
+ const wait = {
224
+ predicate,
225
+ resolve,
226
+ timer: setTimeout(() => this.settleWait(wait, false), timeoutMs),
227
+ interval: setInterval(() => this.checkWaits(), SESSION_POLL_INTERVAL_MS),
228
+ };
229
+ this.pendingWaits.add(wait);
230
+ });
231
+ }
232
+ checkWaits() {
233
+ for (const wait of Array.from(this.pendingWaits)) {
234
+ if (wait.predicate()) {
235
+ this.settleWait(wait, true);
236
+ }
237
+ }
238
+ }
239
+ settleWait(wait, settled) {
240
+ this.pendingWaits.delete(wait);
241
+ clearTimeout(wait.timer);
242
+ clearInterval(wait.interval);
243
+ wait.resolve(settled);
244
+ }
245
+ setSessionState(state) {
246
+ if (this.sessionState === state) {
247
+ return;
248
+ }
249
+ this.sessionState = state;
250
+ this.stateListeners.forEach((listener) => listener(state));
251
+ }
50
252
  }
51
253
  //# sourceMappingURL=iwsdk-adapter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"iwsdk-adapter.js","sourceRoot":"","sources":["../src/iwsdk-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,oBAAoB,GAOrB,MAAM,sBAAsB,CAAC;AAG9B,MAAM,OAAO,YAAY;IAKa;IAJnB,cAAc,GAAG,IAAI,GAAG,EAAiB,CAAC;IAC1C,qBAAqB,GAAG,IAAI,GAAG,EAAwB,CAAC;IACjE,YAAY,GAAwB,oBAAoB,CAAC;IAEjE,YAAoC,KAAqB;QAArB,UAAK,GAAL,KAAK,CAAgB;IAAG,CAAC;IAEtD,OAAO,CAAC,QAAuB;QACpC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAClC,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACvC,CAAC,CAAC;IACJ,CAAC;IAEM,eAAe;QACpB,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAEM,oBAAoB,CAAC,QAA8B;QACxD,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACzC,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9C,CAAC,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACI,QAAQ;QACb,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,kFAAkF;IAC3E,eAAe,CAAC,YAA0C;QAC/D,IAAI,CAAC,YAAY,GAAG,EAAE,GAAG,IAAI,CAAC,YAAY,EAAE,GAAG,YAAY,EAAE,CAAC;QAC9D,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IAChF,CAAC;IAED,sDAAsD;IAC/C,SAAS,CAAC,SAAiB,EAAE,KAAa;QAC/C,MAAM,KAAK,GAAc,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QAC9C,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7D,CAAC;CACF","sourcesContent":["/**\n * IWSDK implementation of {@link RuntimeAdapter}. IWSDK owns the render loop, so\n * the adapter is a passive fan-out: the `ServiceBridgeSystem` (a normal IWSDK\n * system) calls {@link IWSDKAdapter.emitFrame} once per frame and the adapter\n * notifies subscribed services. Capabilities are refined from the XR session via\n * {@link IWSDKAdapter.setCapabilities}.\n */\nimport {\n DEFAULT_CAPABILITIES,\n type AdapterCapabilities,\n type CapabilitiesListener,\n type FrameInfo,\n type FrameListener,\n type RuntimeAdapter,\n type Unsubscribe,\n} from \"./runtime-adapter.js\";\nimport type { IWSDKWorldLike } from \"./iwsdk-host.js\";\n\nexport class IWSDKAdapter implements RuntimeAdapter {\n private readonly frameListeners = new Set<FrameListener>();\n private readonly capabilitiesListeners = new Set<CapabilitiesListener>();\n private capabilities: AdapterCapabilities = DEFAULT_CAPABILITIES;\n\n public constructor(private readonly world: IWSDKWorldLike) {}\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 /**\n * The IWSDK `World` this adapter is bound to. Reserved for capability\n * derivation from the live XR session (see the package README \"Capabilities\"\n * section) - the one piece still to be wired to the real IWSDK session API.\n */\n public getWorld(): IWSDKWorldLike {\n return this.world;\n }\n\n /** Refine capabilities once the XR session reports them; notifies subscribers. */\n public setCapabilities(capabilities: Partial<AdapterCapabilities>): void {\n this.capabilities = { ...this.capabilities, ...capabilities };\n this.capabilitiesListeners.forEach((listener) => listener(this.capabilities));\n }\n\n /** Called once per frame by the ECS bridge system. */\n public emitFrame(timestamp: number, delta: number): void {\n const frame: FrameInfo = { timestamp, delta };\n this.frameListeners.forEach((listener) => listener(frame));\n }\n}\n"]}
1
+ {"version":3,"file":"iwsdk-adapter.js","sourceRoot":"","sources":["../src/iwsdk-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * IWSDK implementation of {@link RuntimeAdapter}. IWSDK owns the render loop, so\n * frames are pushed in rather than pulled: the `ServiceBridgeSystem` (a normal\n * IWSDK system) calls {@link IWSDKAdapter.emitFrame} once per frame and the\n * adapter notifies subscribed services.\n *\n * Capabilities are derived from the live session on construction and on every\n * visibility change, through the core's shared `deriveCapabilities`, with a\n * manual override layer on top. Session lifecycle is\n * exposed through {@link IWSDKAdapter.session}, over the world's `launchXR` and\n * `exitXR` entry points.\n */\nimport {\n DEFAULT_CAPABILITIES,\n DEFAULT_SESSION_TIMEOUT_MS,\n deriveCapabilities,\n type AdapterCapabilities,\n type CapabilitiesListener,\n type FrameInfo,\n type FrameListener,\n type RuntimeAdapter,\n type SessionFacet,\n type SessionMode,\n type SessionRequestOptions,\n type SessionResult,\n type SessionState,\n type SessionVisibility,\n type Unsubscribe,\n} from \"@realitycollective/service-framework\";\nimport type { IWSDKWorldLike } from \"./iwsdk-host.js\";\n\ntype SessionStateListener = (state: SessionState) => void;\ntype SessionVisibilityListener = (visibility: SessionVisibility) => void;\n\n/** How often the session waits re-check the world when the signal is silent. */\nconst SESSION_POLL_INTERVAL_MS = 50;\n\nconst SESSION_VISIBILITY_VALUES: ReadonlySet<string> = new Set([\n \"visible\",\n \"visible-blurred\",\n \"hidden\",\n \"non-immersive\",\n]);\n\n/**\n * IWSDK's visibility signal carries the WebXR visibility strings. Anything this\n * adapter does not recognise is reported as `\"hidden\"`, because treating an\n * unknown state as visible 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/** A `request()` or `end()` waiting for the world to reach some state. */\ninterface PendingWait {\n readonly predicate: () => boolean;\n readonly resolve: (settled: boolean) => void;\n readonly timer: ReturnType<typeof setTimeout>;\n readonly interval: ReturnType<typeof setInterval>;\n}\n\nexport class IWSDKAdapter 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 pendingWaits = new Set<PendingWait>();\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 unsubscribeVisibility: Unsubscribe | undefined;\n\n /** Session lifecycle over the world's `launchXR` / `exitXR` entry points. */\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(private readonly world: IWSDKWorldLike) {\n this.unsubscribeVisibility = world.visibilityState.subscribe?.((value) => {\n this.handleVisibilityChange(value);\n });\n\n this.derived = deriveCapabilities(this.world.session);\n this.capabilities = this.derived;\n this.sessionState = this.hasSession() ? \"active\" : \"none\";\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 IWSDK `World` this adapter is bound to. */\n public getWorld(): IWSDKWorldLike {\n return this.world;\n }\n\n /**\n * Re-read the live session and publish any capability change. The adapter\n * does this itself whenever the visibility signal fires; call it directly on\n * a host whose signal has no `subscribe`, or after the host enables a feature\n * mid-session.\n */\n public refreshCapabilities(): void {\n this.derived = deriveCapabilities(this.world.session);\n this.publishCapabilities();\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 IWSDKAdapter.clearCapabilityOverrides} or\n * {@link IWSDKAdapter.dispose}. Subscribers are notified if the effective\n * capabilities changed.\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 /** Called once per frame by the ECS bridge system. */\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 * Release everything the adapter holds: the visibility subscription, any\n * in-flight session wait, the manual overrides and every listener.\n */\n public dispose(): void {\n this.unsubscribeVisibility?.();\n this.unsubscribeVisibility = undefined;\n\n for (const wait of Array.from(this.pendingWaits)) {\n this.settleWait(wait, false);\n }\n\n this.overrides = {};\n this.frameListeners.clear();\n this.capabilitiesListeners.clear();\n this.stateListeners.clear();\n this.visibilityListeners.clear();\n }\n\n private hasSession(): boolean {\n return Boolean(this.world.session);\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 ) {\n return;\n }\n\n this.capabilities = next;\n this.capabilitiesListeners.forEach((listener) => listener(next));\n }\n\n private handleVisibilityChange(value: unknown): void {\n this.refreshCapabilities();\n this.syncSessionState();\n this.checkWaits();\n\n const visibility = toSessionVisibility(value);\n this.visibilityListeners.forEach((listener) => listener(visibility));\n }\n\n /**\n * Follow the world when nothing local is mid-transition. `request()` and\n * `end()` own the state while they run, so the signal does not fight them.\n */\n private syncSessionState(): void {\n if (this.sessionState === \"requesting\" || this.sessionState === \"ending\") {\n return;\n }\n\n this.setSessionState(this.hasSession() ? \"active\" : \"none\");\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 launch = this.world.launchXR;\n\n if (!launch) {\n return { ok: false, reason: \"unsupported\" };\n }\n\n const timeoutMs = options?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;\n this.setSessionState(\"requesting\");\n\n try {\n // IWSDK reads `XROptions.sessionMode`; its `SessionMode` enum values are the\n // WebXR mode strings, so the facet mode passes through unchanged.\n launch.call(this.world, { sessionMode: mode });\n } catch (error) {\n this.setSessionState(\"none\");\n return { ok: false, reason: \"unsupported\", error };\n }\n\n const started = await this.waitFor(() => this.hasSession(), timeoutMs);\n\n if (!started) {\n this.setSessionState(\"none\");\n return { ok: false, reason: \"timeout\" };\n }\n\n this.setSessionState(\"active\");\n this.refreshCapabilities();\n return { ok: true };\n }\n\n private async endSession(): Promise<void> {\n if (this.sessionState === \"none\") {\n return;\n }\n\n const exit = this.world.exitXR;\n this.setSessionState(\"ending\");\n\n if (exit) {\n exit.call(this.world);\n }\n\n await this.waitFor(() => !this.hasSession(), DEFAULT_SESSION_TIMEOUT_MS);\n this.setSessionState(\"none\");\n this.refreshCapabilities();\n }\n\n /**\n * Resolve `true` once `predicate` holds, `false` on timeout. The visibility\n * signal drives this where the host has one; the interval is the fallback for\n * a host whose signal does not push, and for a session that appears without\n * a visibility transition.\n */\n private waitFor(predicate: () => boolean, timeoutMs: number): Promise<boolean> {\n if (predicate()) {\n return Promise.resolve(true);\n }\n\n return new Promise<boolean>((resolve) => {\n const wait: PendingWait = {\n predicate,\n resolve,\n timer: setTimeout(() => this.settleWait(wait, false), timeoutMs),\n interval: setInterval(() => this.checkWaits(), SESSION_POLL_INTERVAL_MS),\n };\n\n this.pendingWaits.add(wait);\n });\n }\n\n private checkWaits(): void {\n for (const wait of Array.from(this.pendingWaits)) {\n if (wait.predicate()) {\n this.settleWait(wait, true);\n }\n }\n }\n\n private settleWait(wait: PendingWait, settled: boolean): void {\n this.pendingWaits.delete(wait);\n clearTimeout(wait.timer);\n clearInterval(wait.interval);\n wait.resolve(settled);\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"]}
@@ -7,14 +7,52 @@
7
7
  * works without bumping this package. Consumers pass the real `World`,
8
8
  * `createSystem` and `VisibilityState.Visible` value through
9
9
  * {@link makeServiceBridgeSystem}.
10
+ *
11
+ * Every member added here is optional, so a world that only carries the
12
+ * visibility signal - the shape the 1.0.x bridge accepted - still type-checks.
13
+ */
14
+ import type { CapabilityInputSourceLike, CapabilitySessionLike } from "@realitycollective/service-framework";
15
+ /**
16
+ * IWSDK exposes reactive values as `{ value }` signals. The bridge only reads
17
+ * `.value`; the adapter also subscribes when the signal supports it, which is
18
+ * how it learns that a session came or went without polling every frame.
10
19
  */
11
- /** IWSDK exposes reactive values as `{ value }` signals; the bridge only reads `.value`. */
12
20
  export interface IWSDKSignalLike<TValue> {
13
21
  readonly value: TValue;
22
+ /** Optional push notification. Returns an unsubscribe handle. */
23
+ subscribe?(listener: (value: TValue) => void): () => void;
24
+ }
25
+ /** One entry of `XRSession.inputSources`; `hand` is set for a tracked hand. */
26
+ export type IWSDKInputSourceLike = CapabilityInputSourceLike;
27
+ /**
28
+ * The slice of the live `XRSession` the adapter reads to derive capabilities.
29
+ * It is the core's {@link CapabilitySessionLike} with `inputSources` required,
30
+ * because an IWSDK world always carries the collection, and the adapter hands
31
+ * the session straight to the core's `deriveCapabilities`.
32
+ */
33
+ export interface IWSDKSessionLike extends CapabilitySessionLike {
34
+ readonly inputSources: Iterable<IWSDKInputSourceLike>;
35
+ }
36
+ /**
37
+ * The slice of an IWSDK `World` the bridge and adapter read: the visibility
38
+ * signal, the live session, and the session entry points.
39
+ */
40
+ /**
41
+ * The slice of IWSDK `XROptions` the session facet passes to `launchXR`.
42
+ * `sessionMode` carries the WebXR mode string ("immersive-vr", "immersive-ar",
43
+ * "inline"), which is also the value of IWSDK's `SessionMode` enum members.
44
+ */
45
+ export interface IWSDKXROptionsLike {
46
+ readonly sessionMode?: string;
14
47
  }
15
- /** The slice of an IWSDK `World` the bridge reads: the visibility signal. */
16
48
  export interface IWSDKWorldLike<TVisibility = unknown> {
17
49
  readonly visibilityState: IWSDKSignalLike<TVisibility>;
50
+ /** The live XR session, or null/absent when the app is running in 2D. */
51
+ readonly session?: IWSDKSessionLike | null;
52
+ /** IWSDK's session entry point. Absent on a host that cannot start one. */
53
+ launchXR?(options?: IWSDKXROptionsLike): void;
54
+ /** IWSDK's session exit point. Absent on a host that cannot end one. */
55
+ exitXR?(): void;
18
56
  }
19
57
  /** The per-frame entry point IWSDK invokes on a registered system. */
20
58
  export interface IWSDKSystemLike {
@@ -7,6 +7,9 @@
7
7
  * works without bumping this package. Consumers pass the real `World`,
8
8
  * `createSystem` and `VisibilityState.Visible` value through
9
9
  * {@link makeServiceBridgeSystem}.
10
+ *
11
+ * Every member added here is optional, so a world that only carries the
12
+ * visibility signal - the shape the 1.0.x bridge accepted - still type-checks.
10
13
  */
11
14
  export {};
12
15
  //# sourceMappingURL=iwsdk-host.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"iwsdk-host.js","sourceRoot":"","sources":["../src/iwsdk-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG","sourcesContent":["/**\n * Structural contracts for the slice of `@iwsdk/core` this shim touches.\n *\n * Mirrors the approach the three.js and Babylon.js bridges take with their\n * engine packages: the shim never imports `@iwsdk/core` directly, so it builds\n * and unit-tests with no IWSDK / WebXR / headset present, and any 0.4.x IWSDK\n * works without bumping this package. Consumers pass the real `World`,\n * `createSystem` and `VisibilityState.Visible` value through\n * {@link makeServiceBridgeSystem}.\n */\n\n/** IWSDK exposes reactive values as `{ value }` signals; the bridge only reads `.value`. */\nexport interface IWSDKSignalLike<TValue> {\n readonly value: TValue;\n}\n\n/** The slice of an IWSDK `World` the bridge reads: the visibility signal. */\nexport interface IWSDKWorldLike<TVisibility = unknown> {\n readonly visibilityState: IWSDKSignalLike<TVisibility>;\n}\n\n/** The per-frame entry point IWSDK invokes on a registered system. */\nexport interface IWSDKSystemLike {\n update(delta: number, time: number): void;\n}\n\n/** Constructor shape of the base class IWSDK's `createSystem` returns. */\nexport type IWSDKSystemConstructor = new (...args: unknown[]) => IWSDKSystemLike;\n\n/**\n * Structural shape of IWSDK's `createSystem` factory. `createSystem(schema)`\n * returns a base system class that the bridge extends; the bridge passes an\n * empty schema because it queries no ECS components.\n */\nexport type CreateSystemLike = (schema?: Record<string, unknown>) => IWSDKSystemConstructor;\n"]}
1
+ {"version":3,"file":"iwsdk-host.js","sourceRoot":"","sources":["../src/iwsdk-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG","sourcesContent":["/**\n * Structural contracts for the slice of `@iwsdk/core` this shim touches.\n *\n * Mirrors the approach the three.js and Babylon.js bridges take with their\n * engine packages: the shim never imports `@iwsdk/core` directly, so it builds\n * and unit-tests with no IWSDK / WebXR / headset present, and any 0.4.x IWSDK\n * works without bumping this package. Consumers pass the real `World`,\n * `createSystem` and `VisibilityState.Visible` value through\n * {@link makeServiceBridgeSystem}.\n *\n * Every member added here is optional, so a world that only carries the\n * visibility signal - the shape the 1.0.x bridge accepted - still type-checks.\n */\n\nimport type {\n CapabilityInputSourceLike,\n CapabilitySessionLike,\n} from \"@realitycollective/service-framework\";\n\n/**\n * IWSDK exposes reactive values as `{ value }` signals. The bridge only reads\n * `.value`; the adapter also subscribes when the signal supports it, which is\n * how it learns that a session came or went without polling every frame.\n */\nexport interface IWSDKSignalLike<TValue> {\n readonly value: TValue;\n /** Optional push notification. Returns an unsubscribe handle. */\n subscribe?(listener: (value: TValue) => void): () => void;\n}\n\n/** One entry of `XRSession.inputSources`; `hand` is set for a tracked hand. */\nexport type IWSDKInputSourceLike = CapabilityInputSourceLike;\n\n/**\n * The slice of the live `XRSession` the adapter reads to derive capabilities.\n * It is the core's {@link CapabilitySessionLike} with `inputSources` required,\n * because an IWSDK world always carries the collection, and the adapter hands\n * the session straight to the core's `deriveCapabilities`.\n */\nexport interface IWSDKSessionLike extends CapabilitySessionLike {\n readonly inputSources: Iterable<IWSDKInputSourceLike>;\n}\n\n/**\n * The slice of an IWSDK `World` the bridge and adapter read: the visibility\n * signal, the live session, and the session entry points.\n */\n/**\n * The slice of IWSDK `XROptions` the session facet passes to `launchXR`.\n * `sessionMode` carries the WebXR mode string (\"immersive-vr\", \"immersive-ar\",\n * \"inline\"), which is also the value of IWSDK's `SessionMode` enum members.\n */\nexport interface IWSDKXROptionsLike {\n readonly sessionMode?: string;\n}\n\nexport interface IWSDKWorldLike<TVisibility = unknown> {\n readonly visibilityState: IWSDKSignalLike<TVisibility>;\n /** The live XR session, or null/absent when the app is running in 2D. */\n readonly session?: IWSDKSessionLike | null;\n /** IWSDK's session entry point. Absent on a host that cannot start one. */\n launchXR?(options?: IWSDKXROptionsLike): void;\n /** IWSDK's session exit point. Absent on a host that cannot end one. */\n exitXR?(): void;\n}\n\n/** The per-frame entry point IWSDK invokes on a registered system. */\nexport interface IWSDKSystemLike {\n update(delta: number, time: number): void;\n}\n\n/** Constructor shape of the base class IWSDK's `createSystem` returns. */\nexport type IWSDKSystemConstructor = new (...args: unknown[]) => IWSDKSystemLike;\n\n/**\n * Structural shape of IWSDK's `createSystem` factory. `createSystem(schema)`\n * returns a base system class that the bridge extends; the bridge passes an\n * empty schema because it queries no ECS components.\n */\nexport type CreateSystemLike = (schema?: Record<string, unknown>) => IWSDKSystemConstructor;\n"]}
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * The single ECS-services bridge. A normal IWSDK system that, each frame:
3
3
  * - maps IWSDK `visibilityState` to manager focus/pause (auto-pause when the
4
- * headset is removed), and
5
- * - drives the adapter's per-frame fan-out via {@link IWSDKAdapter.emitFrame}.
4
+ * headset is removed),
5
+ * - drives the adapter's per-frame fan-out via {@link IWSDKAdapter.emitFrame},
6
+ * and
7
+ * - emits the scheduler's `renderTick` channel, the same one the three.js and
8
+ * Babylon.js bridges emit, so a service written against the scheduler runs
9
+ * unchanged under IWSDK.
6
10
  *
7
11
  * Game logic is only ticked while the session is visible/focused: in the
8
12
  * browser / 2D preview the host app shows its own gate (e.g. an "Enter VR"
@@ -1,3 +1,5 @@
1
+ /** Milliseconds per second, for the IWSDK seconds-to-scheduler-milliseconds conversion. */
2
+ const MS_PER_SECOND = 1000;
1
3
  /**
2
4
  * Builds the IWSDK `ServiceBridgeSystem` class. Register the returned class with
3
5
  * the world (`world.registerSystem(makeServiceBridgeSystem({ ... }))`); IWSDK
@@ -6,6 +8,7 @@
6
8
  export function makeServiceBridgeSystem(options) {
7
9
  const { adapter, manager, world, createSystem, visibleState } = options;
8
10
  let lastFocused;
11
+ let frame = 0;
9
12
  return class ServiceBridgeSystem extends createSystem({}) {
10
13
  update(delta, time) {
11
14
  const focused = world.visibilityState.value === visibleState;
@@ -18,6 +21,15 @@ export function makeServiceBridgeSystem(options) {
18
21
  // preview and while the headset is removed (visible-blurred).
19
22
  if (focused) {
20
23
  adapter.emitFrame(time, delta);
24
+ // IWSDK reports `delta` in seconds; LifecycleContext.deltaTime is in
25
+ // milliseconds, as the three.js and Babylon.js bridges emit it.
26
+ const context = {
27
+ timestamp: time,
28
+ deltaTime: delta * MS_PER_SECOND,
29
+ frame: ++frame,
30
+ source: "iwsdk",
31
+ };
32
+ manager.scheduler.emit("renderTick", context);
21
33
  }
22
34
  }
23
35
  };
@@ -1 +1 @@
1
- {"version":3,"file":"service-bridge-system.js","sourceRoot":"","sources":["../src/service-bridge-system.ts"],"names":[],"mappings":"AAqCA;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAAgD;IAEhD,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;IACxE,IAAI,WAAgC,CAAC;IAErC,OAAO,MAAM,mBAAoB,SAAQ,YAAY,CAAC,EAAE,CAAC;QACvC,MAAM,CAAC,KAAa,EAAE,IAAY;YAChD,MAAM,OAAO,GAAG,KAAK,CAAC,eAAe,CAAC,KAAK,KAAK,YAAY,CAAC;YAE7D,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;gBAC5B,WAAW,GAAG,OAAO,CAAC;gBACtB,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;gBACjC,OAAO,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;YAChD,CAAC;YAED,oEAAoE;YACpE,8DAA8D;YAC9D,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The single ECS-services bridge. A normal IWSDK system that, each frame:\n * - maps IWSDK `visibilityState` to manager focus/pause (auto-pause when the\n * headset is removed), and\n * - drives the adapter's per-frame fan-out via {@link IWSDKAdapter.emitFrame}.\n *\n * Game logic is only ticked while the session is visible/focused: in the\n * browser / 2D preview the host app shows its own gate (e.g. an \"Enter VR\"\n * overlay) and the services stay idle until the player enters VR.\n *\n * This is the only place the engine loop touches the service layer; services\n * themselves never see IWSDK. The IWSDK primitives (`createSystem` and the\n * `VisibilityState.Visible` value) are injected so this package never imports\n * `@iwsdk/core` - mirroring how the three.js / Babylon.js bridges keep their\n * engine packages at arm's length.\n */\nimport type { ServiceManager } from \"@realitycollective/service-framework\";\nimport type { IWSDKAdapter } from \"./iwsdk-adapter.js\";\nimport type { CreateSystemLike, IWSDKWorldLike } from \"./iwsdk-host.js\";\n\nexport interface ServiceBridgeSystemOptions<TVisibility = unknown> {\n /** The passive frame source fanned out to services. */\n readonly adapter: IWSDKAdapter;\n /** The service manager whose focus/pause signals are driven by visibility. */\n readonly manager: ServiceManager;\n /** The IWSDK world whose `visibilityState` is read each frame. */\n readonly world: IWSDKWorldLike<TVisibility>;\n /** IWSDK's `createSystem` factory (from `@iwsdk/core`). */\n readonly createSystem: CreateSystemLike;\n /**\n * The `VisibilityState` value that means the session is visible/focused\n * (IWSDK `VisibilityState.Visible`). The bridge ticks services only while\n * `world.visibilityState.value === visibleState`.\n */\n readonly visibleState: TVisibility;\n}\n\n/**\n * Builds the IWSDK `ServiceBridgeSystem` class. Register the returned class with\n * the world (`world.registerSystem(makeServiceBridgeSystem({ ... }))`); IWSDK\n * then calls its `update(delta, time)` once per frame.\n */\nexport function makeServiceBridgeSystem<TVisibility>(\n options: ServiceBridgeSystemOptions<TVisibility>,\n) {\n const { adapter, manager, world, createSystem, visibleState } = options;\n let lastFocused: boolean | undefined;\n\n return class ServiceBridgeSystem extends createSystem({}) {\n public override update(delta: number, time: number): void {\n const focused = world.visibilityState.value === visibleState;\n\n if (focused !== lastFocused) {\n lastFocused = focused;\n manager.emitFocusChange(focused);\n manager.emitPauseChange({ paused: !focused });\n }\n\n // Only run game logic while visible/focused; idle in the 2D/browser\n // preview and while the headset is removed (visible-blurred).\n if (focused) {\n adapter.emitFrame(time, delta);\n }\n }\n };\n}\n"]}
1
+ {"version":3,"file":"service-bridge-system.js","sourceRoot":"","sources":["../src/service-bridge-system.ts"],"names":[],"mappings":"AAwBA,2FAA2F;AAC3F,MAAM,aAAa,GAAG,IAAI,CAAC;AAmB3B;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAAgD;IAEhD,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;IACxE,IAAI,WAAgC,CAAC;IACrC,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,OAAO,MAAM,mBAAoB,SAAQ,YAAY,CAAC,EAAE,CAAC;QACvC,MAAM,CAAC,KAAa,EAAE,IAAY;YAChD,MAAM,OAAO,GAAG,KAAK,CAAC,eAAe,CAAC,KAAK,KAAK,YAAY,CAAC;YAE7D,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;gBAC5B,WAAW,GAAG,OAAO,CAAC;gBACtB,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;gBACjC,OAAO,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;YAChD,CAAC;YAED,oEAAoE;YACpE,8DAA8D;YAC9D,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAE/B,qEAAqE;gBACrE,gEAAgE;gBAChE,MAAM,OAAO,GAAqB;oBAChC,SAAS,EAAE,IAAI;oBACf,SAAS,EAAE,KAAK,GAAG,aAAa;oBAChC,KAAK,EAAE,EAAE,KAAK;oBACd,MAAM,EAAE,OAAO;iBAChB,CAAC;gBAEF,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The single ECS-services bridge. A normal IWSDK system that, each frame:\n * - maps IWSDK `visibilityState` to manager focus/pause (auto-pause when the\n * headset is removed),\n * - drives the adapter's per-frame fan-out via {@link IWSDKAdapter.emitFrame},\n * and\n * - emits the scheduler's `renderTick` channel, the same one the three.js and\n * Babylon.js bridges emit, so a service written against the scheduler runs\n * unchanged under IWSDK.\n *\n * Game logic is only ticked while the session is visible/focused: in the\n * browser / 2D preview the host app shows its own gate (e.g. an \"Enter VR\"\n * overlay) and the services stay idle until the player enters VR.\n *\n * This is the only place the engine loop touches the service layer; services\n * themselves never see IWSDK. The IWSDK primitives (`createSystem` and the\n * `VisibilityState.Visible` value) are injected so this package never imports\n * `@iwsdk/core` - mirroring how the three.js / Babylon.js bridges keep their\n * engine packages at arm's length.\n */\nimport type { LifecycleContext, ServiceManager } from \"@realitycollective/service-framework\";\nimport type { IWSDKAdapter } from \"./iwsdk-adapter.js\";\nimport type { CreateSystemLike, IWSDKWorldLike } from \"./iwsdk-host.js\";\n\n/** Milliseconds per second, for the IWSDK seconds-to-scheduler-milliseconds conversion. */\nconst MS_PER_SECOND = 1000;\n\nexport interface ServiceBridgeSystemOptions<TVisibility = unknown> {\n /** The passive frame source fanned out to services. */\n readonly adapter: IWSDKAdapter;\n /** The service manager whose focus/pause signals are driven by visibility. */\n readonly manager: ServiceManager;\n /** The IWSDK world whose `visibilityState` is read each frame. */\n readonly world: IWSDKWorldLike<TVisibility>;\n /** IWSDK's `createSystem` factory (from `@iwsdk/core`). */\n readonly createSystem: CreateSystemLike;\n /**\n * The `VisibilityState` value that means the session is visible/focused\n * (IWSDK `VisibilityState.Visible`). The bridge ticks services only while\n * `world.visibilityState.value === visibleState`.\n */\n readonly visibleState: TVisibility;\n}\n\n/**\n * Builds the IWSDK `ServiceBridgeSystem` class. Register the returned class with\n * the world (`world.registerSystem(makeServiceBridgeSystem({ ... }))`); IWSDK\n * then calls its `update(delta, time)` once per frame.\n */\nexport function makeServiceBridgeSystem<TVisibility>(\n options: ServiceBridgeSystemOptions<TVisibility>,\n) {\n const { adapter, manager, world, createSystem, visibleState } = options;\n let lastFocused: boolean | undefined;\n let frame = 0;\n\n return class ServiceBridgeSystem extends createSystem({}) {\n public override update(delta: number, time: number): void {\n const focused = world.visibilityState.value === visibleState;\n\n if (focused !== lastFocused) {\n lastFocused = focused;\n manager.emitFocusChange(focused);\n manager.emitPauseChange({ paused: !focused });\n }\n\n // Only run game logic while visible/focused; idle in the 2D/browser\n // preview and while the headset is removed (visible-blurred).\n if (focused) {\n adapter.emitFrame(time, delta);\n\n // IWSDK reports `delta` in seconds; LifecycleContext.deltaTime is in\n // milliseconds, as the three.js and Babylon.js bridges emit it.\n const context: LifecycleContext = {\n timestamp: time,\n deltaTime: delta * MS_PER_SECOND,\n frame: ++frame,\n source: \"iwsdk\",\n };\n\n manager.scheduler.emit(\"renderTick\", context);\n }\n }\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/service-framework-iwsdk",
3
- "version": "1.0.1-preview.0",
3
+ "version": "1.0.1-preview.2",
4
4
  "description": "Meta IWSDK (WebXR) frame-source bindings for the Reality Collective TypeScript Service Framework.",
5
5
  "author": "Reality Collective",
6
6
  "license": "MIT",
@@ -35,16 +35,8 @@
35
35
  "bugs": {
36
36
  "url": "https://github.com/realitycollective/com.realitycollective.service-framework.ts/issues"
37
37
  },
38
- "peerDependencies": {
39
- "@iwsdk/core": ">=0.4.0 <0.5.0"
40
- },
41
- "peerDependenciesMeta": {
42
- "@iwsdk/core": {
43
- "optional": true
44
- }
45
- },
46
38
  "dependencies": {
47
- "@realitycollective/service-framework": "^1.0.1-preview.0"
39
+ "@realitycollective/service-framework": "^1.0.1-preview.2"
48
40
  },
49
41
  "scripts": {
50
42
  "build": "tsc -p tsconfig.build.json",
@@ -1,20 +0,0 @@
1
- /**
2
- * Headless {@link RuntimeAdapter} for tests. Services run against this with no
3
- * IWSDK / renderer / headset; tests drive the loop by calling
4
- * {@link MockRuntimeAdapter.emitFrame} and refine gating with
5
- * {@link MockRuntimeAdapter.setCapabilities}.
6
- */
7
- import { type AdapterCapabilities, type CapabilitiesListener, type FrameListener, type RuntimeAdapter, type Unsubscribe } from "./runtime-adapter.js";
8
- export declare class MockRuntimeAdapter implements RuntimeAdapter {
9
- private readonly frameListeners;
10
- private readonly capabilitiesListeners;
11
- private capabilities;
12
- constructor(capabilities?: Partial<AdapterCapabilities>);
13
- onFrame(listener: FrameListener): Unsubscribe;
14
- getCapabilities(): AdapterCapabilities;
15
- onCapabilitiesChange(listener: CapabilitiesListener): Unsubscribe;
16
- /** Refine capabilities in tests; notifies subscribers. */
17
- setCapabilities(capabilities: Partial<AdapterCapabilities>): void;
18
- /** Drive a frame in tests. */
19
- emitFrame(timestamp?: number, delta?: number): void;
20
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * Headless {@link RuntimeAdapter} for tests. Services run against this with no
3
- * IWSDK / renderer / headset; tests drive the loop by calling
4
- * {@link MockRuntimeAdapter.emitFrame} and refine gating with
5
- * {@link MockRuntimeAdapter.setCapabilities}.
6
- */
7
- import { DEFAULT_CAPABILITIES, } from "./runtime-adapter.js";
8
- export class MockRuntimeAdapter {
9
- frameListeners = new Set();
10
- capabilitiesListeners = new Set();
11
- capabilities;
12
- constructor(capabilities = {}) {
13
- this.capabilities = { ...DEFAULT_CAPABILITIES, ...capabilities };
14
- }
15
- onFrame(listener) {
16
- this.frameListeners.add(listener);
17
- return () => {
18
- this.frameListeners.delete(listener);
19
- };
20
- }
21
- getCapabilities() {
22
- return this.capabilities;
23
- }
24
- onCapabilitiesChange(listener) {
25
- this.capabilitiesListeners.add(listener);
26
- return () => {
27
- this.capabilitiesListeners.delete(listener);
28
- };
29
- }
30
- /** Refine capabilities in tests; notifies subscribers. */
31
- setCapabilities(capabilities) {
32
- this.capabilities = { ...this.capabilities, ...capabilities };
33
- this.capabilitiesListeners.forEach((listener) => listener(this.capabilities));
34
- }
35
- /** Drive a frame in tests. */
36
- emitFrame(timestamp = 0, delta = 1 / 72) {
37
- this.frameListeners.forEach((listener) => listener({ timestamp, delta }));
38
- }
39
- }
40
- //# sourceMappingURL=mock-runtime-adapter.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mock-runtime-adapter.js","sourceRoot":"","sources":["../src/mock-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,oBAAoB,GAMrB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,OAAO,kBAAkB;IACZ,cAAc,GAAG,IAAI,GAAG,EAAiB,CAAC;IAC1C,qBAAqB,GAAG,IAAI,GAAG,EAAwB,CAAC;IACjE,YAAY,CAAsB;IAE1C,YAAmB,eAA6C,EAAE;QAChE,IAAI,CAAC,YAAY,GAAG,EAAE,GAAG,oBAAoB,EAAE,GAAG,YAAY,EAAE,CAAC;IACnE,CAAC;IAEM,OAAO,CAAC,QAAuB;QACpC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAClC,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACvC,CAAC,CAAC;IACJ,CAAC;IAEM,eAAe;QACpB,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAEM,oBAAoB,CAAC,QAA8B;QACxD,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACzC,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9C,CAAC,CAAC;IACJ,CAAC;IAED,0DAA0D;IACnD,eAAe,CAAC,YAA0C;QAC/D,IAAI,CAAC,YAAY,GAAG,EAAE,GAAG,IAAI,CAAC,YAAY,EAAE,GAAG,YAAY,EAAE,CAAC;QAC9D,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IAChF,CAAC;IAED,8BAA8B;IACvB,SAAS,CAAC,SAAS,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,GAAG,EAAE;QAC5C,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IAC5E,CAAC;CACF","sourcesContent":["/**\n * Headless {@link RuntimeAdapter} for tests. Services run against this with no\n * IWSDK / renderer / headset; tests drive the loop by calling\n * {@link MockRuntimeAdapter.emitFrame} and refine gating with\n * {@link MockRuntimeAdapter.setCapabilities}.\n */\nimport {\n DEFAULT_CAPABILITIES,\n type AdapterCapabilities,\n type CapabilitiesListener,\n type FrameListener,\n type RuntimeAdapter,\n type Unsubscribe,\n} from \"./runtime-adapter.js\";\n\nexport class MockRuntimeAdapter implements RuntimeAdapter {\n private readonly frameListeners = new Set<FrameListener>();\n private readonly capabilitiesListeners = new Set<CapabilitiesListener>();\n private capabilities: AdapterCapabilities;\n\n public constructor(capabilities: Partial<AdapterCapabilities> = {}) {\n this.capabilities = { ...DEFAULT_CAPABILITIES, ...capabilities };\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 /** Refine capabilities in tests; notifies subscribers. */\n public setCapabilities(capabilities: Partial<AdapterCapabilities>): void {\n this.capabilities = { ...this.capabilities, ...capabilities };\n this.capabilitiesListeners.forEach((listener) => listener(this.capabilities));\n }\n\n /** Drive a frame in tests. */\n public emitFrame(timestamp = 0, delta = 1 / 72): void {\n this.frameListeners.forEach((listener) => listener({ timestamp, delta }));\n }\n}\n"]}
@@ -1,38 +0,0 @@
1
- /**
2
- * Runtime adapter contract (IWSDK-only, minimal frame-source).
3
- *
4
- * Trimmed to what an IWSDK-only client needs: a per-frame fan-out and
5
- * capability flags. IWSDK already owns sessions, input and rendering, so the
6
- * adapter deliberately does NOT re-abstract those. {@link MockRuntimeAdapter}
7
- * implements this same interface so services can be unit-tested headless.
8
- */
9
- export type Unsubscribe = () => void;
10
- export interface FrameInfo {
11
- /** Frame timestamp in milliseconds (IWSDK system `time`). */
12
- readonly timestamp: number;
13
- /** Seconds elapsed since the previous frame (IWSDK system `delta`). */
14
- readonly delta: number;
15
- }
16
- /** XR capabilities services gate on (e.g. passthrough requires `immersive`). */
17
- export interface AdapterCapabilities {
18
- readonly immersive: boolean;
19
- readonly handTracking: boolean;
20
- readonly planeDetection: boolean;
21
- readonly passthrough: boolean;
22
- }
23
- export declare const DEFAULT_CAPABILITIES: AdapterCapabilities;
24
- export type FrameListener = (frame: FrameInfo) => void;
25
- export type CapabilitiesListener = (capabilities: AdapterCapabilities) => void;
26
- export interface RuntimeAdapter {
27
- /** Subscribe to per-frame updates; returns an unsubscribe handle. */
28
- onFrame(listener: FrameListener): Unsubscribe;
29
- /** Current XR capabilities (may change once a session is established). */
30
- getCapabilities(): AdapterCapabilities;
31
- /**
32
- * Subscribe to capability changes; returns an unsubscribe handle. Mirrors
33
- * {@link RuntimeAdapter.onFrame} so gating services can react to a session
34
- * coming online instead of polling {@link RuntimeAdapter.getCapabilities}
35
- * every frame.
36
- */
37
- onCapabilitiesChange(listener: CapabilitiesListener): Unsubscribe;
38
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Runtime adapter contract (IWSDK-only, minimal frame-source).
3
- *
4
- * Trimmed to what an IWSDK-only client needs: a per-frame fan-out and
5
- * capability flags. IWSDK already owns sessions, input and rendering, so the
6
- * adapter deliberately does NOT re-abstract those. {@link MockRuntimeAdapter}
7
- * implements this same interface so services can be unit-tested headless.
8
- */
9
- export const DEFAULT_CAPABILITIES = {
10
- immersive: false,
11
- handTracking: false,
12
- planeDetection: false,
13
- passthrough: false,
14
- };
15
- //# sourceMappingURL=runtime-adapter.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime-adapter.js","sourceRoot":"","sources":["../src/runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmBH,MAAM,CAAC,MAAM,oBAAoB,GAAwB;IACvD,SAAS,EAAE,KAAK;IAChB,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,KAAK;IACrB,WAAW,EAAE,KAAK;CACnB,CAAC","sourcesContent":["/**\n * Runtime adapter contract (IWSDK-only, minimal frame-source).\n *\n * Trimmed to what an IWSDK-only client needs: a per-frame fan-out and\n * capability flags. IWSDK already owns sessions, input and rendering, so the\n * adapter deliberately does NOT re-abstract those. {@link MockRuntimeAdapter}\n * implements this same interface so services can be unit-tested headless.\n */\n\nexport type Unsubscribe = () => void;\n\nexport interface FrameInfo {\n /** Frame timestamp in milliseconds (IWSDK system `time`). */\n readonly timestamp: number;\n /** Seconds elapsed since the previous frame (IWSDK system `delta`). */\n readonly delta: number;\n}\n\n/** XR capabilities services gate on (e.g. passthrough requires `immersive`). */\nexport interface AdapterCapabilities {\n readonly immersive: boolean;\n readonly handTracking: boolean;\n readonly planeDetection: boolean;\n readonly passthrough: boolean;\n}\n\nexport const DEFAULT_CAPABILITIES: AdapterCapabilities = {\n immersive: false,\n handTracking: false,\n planeDetection: false,\n passthrough: false,\n};\n\nexport type FrameListener = (frame: FrameInfo) => void;\nexport type CapabilitiesListener = (capabilities: AdapterCapabilities) => void;\n\nexport interface RuntimeAdapter {\n /** Subscribe to per-frame updates; returns an unsubscribe handle. */\n onFrame(listener: FrameListener): Unsubscribe;\n /** Current XR capabilities (may change once a session is established). */\n getCapabilities(): AdapterCapabilities;\n /**\n * Subscribe to capability changes; returns an unsubscribe handle. Mirrors\n * {@link RuntimeAdapter.onFrame} so gating services can react to a session\n * coming online instead of polling {@link RuntimeAdapter.getCapabilities}\n * every frame.\n */\n onCapabilitiesChange(listener: CapabilitiesListener): Unsubscribe;\n}\n"]}
@@ -1,27 +0,0 @@
1
- /**
2
- * `SnapshotService` base - the "services own state" pattern. A snapshot service
3
- * owns one immutable state object and a pub/sub list: consumers (other services,
4
- * or the ECS presentation layer) subscribe and receive the current value
5
- * immediately, then every subsequent publish.
6
- *
7
- * It has no IWSDK dependency - only `@realitycollective/service-framework` - so
8
- * services that extend it stay unit-testable headless against
9
- * {@link MockRuntimeAdapter}.
10
- */
11
- import { BaseService } from "@realitycollective/service-framework";
12
- /**
13
- * The activation-context type a service constructor receives. Aliased here so
14
- * every service uses one consistent, framework-correct shape (matches the
15
- * `useFactory(context)` parameter the ServiceManager passes).
16
- */
17
- export type ServiceContext<TConfig = unknown> = ConstructorParameters<typeof BaseService<TConfig>>[0];
18
- export type SnapshotListener<TSnapshot> = (snapshot: TSnapshot) => void;
19
- export declare abstract class SnapshotService<TConfig, TSnapshot> extends BaseService<TConfig> {
20
- protected snapshot: TSnapshot;
21
- private readonly listeners;
22
- protected constructor(context: ServiceContext<TConfig>, initialSnapshot: TSnapshot);
23
- getSnapshot(): TSnapshot;
24
- subscribe(listener: SnapshotListener<TSnapshot>): () => void;
25
- protected publishSnapshot(snapshot: TSnapshot): void;
26
- protected updateSnapshot(partial: Partial<TSnapshot>): void;
27
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * `SnapshotService` base - the "services own state" pattern. A snapshot service
3
- * owns one immutable state object and a pub/sub list: consumers (other services,
4
- * or the ECS presentation layer) subscribe and receive the current value
5
- * immediately, then every subsequent publish.
6
- *
7
- * It has no IWSDK dependency - only `@realitycollective/service-framework` - so
8
- * services that extend it stay unit-testable headless against
9
- * {@link MockRuntimeAdapter}.
10
- */
11
- import { BaseService } from "@realitycollective/service-framework";
12
- export class SnapshotService extends BaseService {
13
- snapshot;
14
- listeners = new Set();
15
- constructor(context, initialSnapshot) {
16
- super(context);
17
- this.snapshot = initialSnapshot;
18
- }
19
- getSnapshot() {
20
- return this.snapshot;
21
- }
22
- subscribe(listener) {
23
- this.listeners.add(listener);
24
- listener(this.snapshot);
25
- return () => {
26
- this.listeners.delete(listener);
27
- };
28
- }
29
- publishSnapshot(snapshot) {
30
- this.snapshot = snapshot;
31
- this.listeners.forEach((listener) => listener(this.snapshot));
32
- }
33
- updateSnapshot(partial) {
34
- this.publishSnapshot({ ...this.snapshot, ...partial });
35
- }
36
- }
37
- //# sourceMappingURL=snapshot-service.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"snapshot-service.js","sourceRoot":"","sources":["../src/snapshot-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AAYnE,MAAM,OAAgB,eAAoC,SAAQ,WAAoB;IAC1E,QAAQ,CAAY;IACb,SAAS,GAAG,IAAI,GAAG,EAA+B,CAAC;IAEpE,YAAsB,OAAgC,EAAE,eAA0B;QAChF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,QAAQ,GAAG,eAAe,CAAC;IAClC,CAAC;IAEM,WAAW;QAChB,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAEM,SAAS,CAAC,QAAqC;QACpD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxB,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC,CAAC;IACJ,CAAC;IAES,eAAe,CAAC,QAAmB;QAC3C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAChE,CAAC;IAES,cAAc,CAAC,OAA2B;QAClD,IAAI,CAAC,eAAe,CAAC,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IACzD,CAAC;CACF","sourcesContent":["/**\n * `SnapshotService` base - the \"services own state\" pattern. A snapshot service\n * owns one immutable state object and a pub/sub list: consumers (other services,\n * or the ECS presentation layer) subscribe and receive the current value\n * immediately, then every subsequent publish.\n *\n * It has no IWSDK dependency - only `@realitycollective/service-framework` - so\n * services that extend it stay unit-testable headless against\n * {@link MockRuntimeAdapter}.\n */\nimport { BaseService } from \"@realitycollective/service-framework\";\n\n/**\n * The activation-context type a service constructor receives. Aliased here so\n * every service uses one consistent, framework-correct shape (matches the\n * `useFactory(context)` parameter the ServiceManager passes).\n */\nexport type ServiceContext<TConfig = unknown> =\n ConstructorParameters<typeof BaseService<TConfig>>[0];\n\nexport type SnapshotListener<TSnapshot> = (snapshot: TSnapshot) => void;\n\nexport abstract class SnapshotService<TConfig, TSnapshot> extends BaseService<TConfig> {\n protected snapshot: TSnapshot;\n private readonly listeners = new Set<SnapshotListener<TSnapshot>>();\n\n protected constructor(context: ServiceContext<TConfig>, initialSnapshot: TSnapshot) {\n super(context);\n this.snapshot = initialSnapshot;\n }\n\n public getSnapshot(): TSnapshot {\n return this.snapshot;\n }\n\n public subscribe(listener: SnapshotListener<TSnapshot>): () => void {\n this.listeners.add(listener);\n listener(this.snapshot);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n protected publishSnapshot(snapshot: TSnapshot): void {\n this.snapshot = snapshot;\n this.listeners.forEach((listener) => listener(this.snapshot));\n }\n\n protected updateSnapshot(partial: Partial<TSnapshot>): void {\n this.publishSnapshot({ ...this.snapshot, ...partial });\n }\n}\n"]}