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

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
@@ -4,6 +4,27 @@ Change log for the Reality Collective Service Framework for TypeScript. All pack
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
6
6
 
7
+ ## [1.0.2]
8
+
9
+ A native platform, beside IWSDK, three.js and Babylon.js, and byte I/O that works the same on the web and on a native host.
10
+
11
+ ### Added
12
+
13
+ - `@realitycollective/service-framework-native`, the binding for a native XR app that embeds a JavaScript engine such as Hermes. The app owns the session, the frame loop, rendering and physics, and installs one host object, `globalThis.__rcHost`, before it evaluates the bundle. Everything crosses that object as plain values. `NativeRuntimeAdapter` publishes the app's frames and session through the same `RuntimeAdapter` seam as the other bindings, with the session facet, and passes `runtimeAdapterContractCases()`. It derives capabilities from the OpenXR session state, the enabled extensions, system hand tracking support and the blend mode. It keeps the same sticky `setCapabilities` override, `clearCapabilityOverrides()` and `refreshCapabilities()` the other adapters have. Given a `scheduler`, it emits `renderTick` with `source: "native"`. `createNativeHostIO()` returns a `HostIO` over the host's `io` slice. The host object's shape was proven on a Quest 3 and in the visionOS Simulator by the team that built the first native host. The package README documents it, including the slices the other families' native packages read.
14
+ - `HostIO` in the core, with `createWebHostIO()` over `fetch` and `DecompressionStream`. A service that reads assets takes a `HostIO` and never reaches for a global, so the same code runs on the web and on a native host. The globals are read when a method is called, so a host without them fails only if it uses them. Helpers that work on both: `fetchText`, `fetchJson`, `fetchMaybeGzipped` (gunzips only bytes that carry the gzip magic number, since hosts differ on whether they undo `Content-Encoding`), `decodeUtf8` (no `TextDecoder`, which Hermes lacks), `isGzip` and `concatBytes`. This is transport only. Decoding images, audio and models stays with the engine.
15
+ - A "Host requirements" section in the core package README. It names every global the core expects from its host beyond the ECMAScript language: `setTimeout` and `clearTimeout` for `resolveAsync`, `waitUntilInitialized` and the mock adapter's session request, and `setInterval` and `clearInterval` for a `TimerScheduler` built without injected timers. It names the two globals used only when present, `AbortController` (with a built-in fallback) and the `fetch` and `DecompressionStream` behind `createWebHostIO()`, and states that nothing else is assumed. A host that embeds a bare engine, such as Hermes in a native app, can now supply exactly that list. `test/host-requirements.test.ts` checks the table against the source, so a new host global fails the suite until it is listed.
16
+ - The capability override rule, stated in the README and on the `RuntimeAdapter` doc comment. Every adapter exposes `setCapabilities(partial)` as a sticky override over what it derives, and an adapter that derives also exposes `clearCapabilityOverrides()`. Every in-repo adapter already did, and every conformance driver already wired its `capabilities` hook to it. The rule was never written down, so an adapter written elsewhere that only derived from its host failed the capability-change case. The method stays off the `RuntimeAdapter` interface, because services must not call it. The same test checks that every in-repo adapter still exposes it.
17
+
18
+ - `hostIOContractCases()` and `renderTickContractCases()`, shared conformance suites shipped as data, like `runtimeAdapterContractCases()`. Clients only ever use the core: a service reads assets through `HostIO` and subscribes to `renderTick` on the core scheduler, and never asks which platform is underneath. That only holds if every platform behaves the same way, so every platform now runs the same checks. `hostIOContractCases()` runs against `createWebHostIO()` and `createNativeHostIO()`: served bytes come back exactly, a missing resource rejects, gunzip restores and rejects non-gzip, and the core helpers read plain and gzipped JSON alike. It needs no `CompressionStream` or `TextEncoder`, so a native app can run it on device. `renderTickContractCases()` runs against `ThreeRenderLoopBridge`, `WebXRRuntimeAdapter`, `BabylonRenderLoopBridge`, `BabylonRuntimeAdapter`, the IWSDK service bridge system and `NativeRuntimeAdapter`: one tick per frame, frames counted from 1, a stable source, and `timestamp` and `deltaTime` in milliseconds.
19
+ - `LifecycleContext` now documents its units: `timestamp` and `deltaTime` in milliseconds, `frame` counted from 1, and `source` naming the binding.
20
+
21
+ ### Fixed
22
+
23
+ - The IWSDK bridge reported `timestamp` in seconds. IWSDK calls a system's `update(delta, time)` with `time` as elapsed seconds from three.js `Clock`, and the bridge passed it straight into `FrameInfo.timestamp` and `LifecycleContext.timestamp`, which are milliseconds on every other platform. A service reading `timestamp` got a value a thousand times smaller on IWSDK, which is a platform conditional a client should never need. The bridge now converts it, and `renderTickContractCases()` fails a binding that does not. The `FrameInfo` doc no longer cites IWSDK as the source of its unit. Code that read the IWSDK timestamp as seconds must now read milliseconds.
24
+ - `createWebHostIO().gunzip` left the stream's write-side error unhandled when given bytes that are not gzip, so a host logged an unhandled rejection besides the rejection the caller received. It now rejects once. The new `HostIO` suite found it.
25
+ - `TimerScheduler` counted frames on one counter shared by all four of its channels, and named `source` after the channel. A service counting render frames therefore saw gaps that depended on how often the other channels fired, where every engine binding counts 1, 2, 3. Each channel now counts its own ticks from 1, and every context names `"timer"` as its source, as a binding names itself. The timer now runs the shared `renderTickContractCases()` suite, which fails on the old shared counter.
26
+ - `ServiceManager` no longer needs a global `AbortController`. Every service record created one unconditionally to hand the service its `signal`, so a host without it failed to boot any profile before client code ran. Hermes, the engine a native host embeds, is such a host: `ReferenceError: Property 'AbortController' doesn't exist`. The manager now uses the global when it exists and otherwise falls back to a small internal controller. Its signal carries `aborted`, `reason`, `onabort`, `addEventListener("abort")` and `removeEventListener`, and `abort()` fires once. Web and Node behaviour is unchanged, because both have the global. `ServiceActivationContext.signal` is still typed `AbortSignal`, and nothing was added to the public exports.
27
+
7
28
  ## [1.0.1] - 2026-09-17
8
29
 
9
30
  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, and one interface changed shape: `AdapterCapabilities` gains a required `environmentBlendMode` key, so code that builds the whole object by hand - an adapter of your own, or a test fake - has one more field to supply. Code that only reads capabilities is unaffected.
@@ -20,11 +20,15 @@ export function makeServiceBridgeSystem(options) {
20
20
  // Only run game logic while visible/focused; idle in the 2D/browser
21
21
  // preview and while the headset is removed (visible-blurred).
22
22
  if (focused) {
23
- adapter.emitFrame(time, delta);
23
+ // IWSDK reports `time` as elapsed seconds (three.js `Clock`), but
24
+ // `FrameInfo.timestamp` and `LifecycleContext.timestamp` are
25
+ // milliseconds on every platform, so both are converted here.
26
+ const timestamp = time * MS_PER_SECOND;
27
+ adapter.emitFrame(timestamp, delta);
24
28
  // IWSDK reports `delta` in seconds; LifecycleContext.deltaTime is in
25
29
  // milliseconds, as the three.js and Babylon.js bridges emit it.
26
30
  const context = {
27
- timestamp: time,
31
+ timestamp,
28
32
  deltaTime: delta * MS_PER_SECOND,
29
33
  frame: ++frame,
30
34
  source: "iwsdk",
@@ -1 +1 @@
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"]}
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,kEAAkE;gBAClE,6DAA6D;gBAC7D,8DAA8D;gBAC9D,MAAM,SAAS,GAAG,IAAI,GAAG,aAAa,CAAC;gBACvC,OAAO,CAAC,SAAS,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;gBAEpC,qEAAqE;gBACrE,gEAAgE;gBAChE,MAAM,OAAO,GAAqB;oBAChC,SAAS;oBACT,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 // IWSDK reports `time` as elapsed seconds (three.js `Clock`), but\n // `FrameInfo.timestamp` and `LifecycleContext.timestamp` are\n // milliseconds on every platform, so both are converted here.\n const timestamp = time * MS_PER_SECOND;\n adapter.emitFrame(timestamp, 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,\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",
3
+ "version": "1.0.2-preview.0",
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",
@@ -36,7 +36,7 @@
36
36
  "url": "https://github.com/realitycollective/com.realitycollective.service-framework.ts/issues"
37
37
  },
38
38
  "dependencies": {
39
- "@realitycollective/service-framework": "^1.0.1"
39
+ "@realitycollective/service-framework": "^1.0.2-preview.0"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.build.json",