From fd629092b1e5b0c553a42f6853ea3ef116cbc3cb Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:06:52 -0300 Subject: [PATCH 01/14] Declare what the package needs: RN 0.85 peers, engines, sideEffects, alpha version --- Splatkit.podspec | 2 +- package.json | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Splatkit.podspec b/Splatkit.podspec index 5a19908..bd47bdc 100644 --- a/Splatkit.podspec +++ b/Splatkit.podspec @@ -11,7 +11,7 @@ Pod::Spec.new do |s| s.authors = package["author"] s.platforms = { :ios => min_ios_version_supported } - s.source = { :git => "https://github.com/Xget7/react-native-splatkit.git", :tag => "#{s.version}" } + s.source = { :git => "https://github.com/Xget7/react-native-splatkit.git", :tag => "v#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift,cpp}" s.private_header_files = "ios/**/*.h" diff --git a/package.json b/package.json index bf689a3..089c046 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-splatkit", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "description": "React Native component for real-time Gaussian splatting: load an SPZ scene, pick a quality preset, and walk through it from JavaScript.", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", @@ -17,9 +17,7 @@ "lib", "android", "ios", - "cpp", "*.podspec", - "react-native.config.js", "!ios/build", "!android/build", "!android/gradle", @@ -36,7 +34,8 @@ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", "prepare": "bob build", "typecheck": "tsc", - "lint": "eslint \"**/*.{js,ts,tsx}\"" + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "test": "jest" }, "keywords": [ "react-native", @@ -56,12 +55,18 @@ }, "author": "Juan Tupa (https://github.com/Xget7)", "license": "MIT", + "sideEffects": false, + "engines": { + "node": ">= 22.11.0" + }, "bugs": { "url": "https://github.com/Xget7/react-native-splatkit/issues" }, "homepage": "https://github.com/Xget7/react-native-splatkit#readme", "publishConfig": { - "registry": "https://registry.npmjs.org/" + "registry": "https://registry.npmjs.org/", + "access": "public", + "tag": "alpha" }, "devDependencies": { "@eslint/compat": "^2.1.0", @@ -83,8 +88,8 @@ "typescript": "^6.0.3" }, "peerDependencies": { - "react": "*", - "react-native": "*" + "react": ">=19.0.0", + "react-native": ">=0.85.0" }, "workspaces": [ "example" @@ -110,7 +115,7 @@ }, "codegenConfig": { "name": "SplatViewSpec", - "type": "all", + "type": "components", "jsSrcsDir": "src", "android": { "javaPackageName": "com.splatkit.reactnative" From c7ae3d7969bcd6484b59e9056589a287c8a5a038 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:07:52 -0300 Subject: [PATCH 02/14] Pin minSdk 29 in the library so a lower app floor fails with this package's name --- android/build.gradle | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 52802ac..f6251e1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,9 +1,6 @@ buildscript { ext.Splatkit = [ kotlinVersion: "2.1.21", - // The engine needs Android 10 and Vulkan 1.1; there is no fallback renderer - // below that, so the floor is the engine's floor and not React Native's. - minSdkVersion: 29, compileSdkVersion: 36, // The one coupling between this package and the engine. Nothing else about // the renderer is visible from here. @@ -42,7 +39,11 @@ android { compileSdkVersion getExtOrDefault("compileSdkVersion") defaultConfig { - minSdkVersion getExtOrDefault("minSdkVersion") + // The engine needs Android 10 and Vulkan 1.1; there is no fallback renderer + // below that. This is pinned rather than read from the app so that an app + // on a lower floor fails here, with this package's name in the message, + // instead of deep inside the engine's manifest merge. + minSdkVersion 29 } compileOptions { From def58749f1e38fa88a2a5bd6394679be61028e66 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:09:54 -0300 Subject: [PATCH 03/14] Normalise quality in JavaScript: typed presets, clamped ranges, sentinels for unset overrides --- jest.config.js | 7 + package.json | 3 + src/SplatView.tsx | 76 +-- src/SplatViewNativeComponent.ts | 45 +- src/__tests__/quality.test.ts | 83 ++++ src/index.tsx | 2 + src/quality.ts | 125 +++++ tsconfig.build.json | 2 +- yarn.lock | 798 +++++++++++++++++++++++++++++++- 9 files changed, 1068 insertions(+), 73 deletions(-) create mode 100644 jest.config.js create mode 100644 src/__tests__/quality.test.ts create mode 100644 src/quality.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..0dad826 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,7 @@ +/** Pure unit tests for the JavaScript side; nothing here imports react-native. */ +module.exports = { + testEnvironment: 'node', + roots: ['/src'], + transform: { '\\.[jt]sx?$': 'babel-jest' }, + modulePathIgnorePatterns: ['/lib/', '/example/'], +}; diff --git a/package.json b/package.json index 089c046..cf3d6c7 100644 --- a/package.json +++ b/package.json @@ -74,12 +74,15 @@ "@eslint/js": "^10.0.1", "@react-native/babel-preset": "0.85.0", "@react-native/eslint-config": "0.85.0", + "@types/jest": "^29.5.14", "@types/react": "^19.2.0", + "babel-jest": "^29.7.0", "del-cli": "^7.0.0", "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "eslint-plugin-ft-flow": "^3.0.11", "eslint-plugin-prettier": "^5.5.6", + "jest": "^29.7.0", "prettier": "^3.8.3", "react": "19.2.3", "react-native": "0.85.0", diff --git a/src/SplatView.tsx b/src/SplatView.tsx index 5c4388f..8191048 100644 --- a/src/SplatView.tsx +++ b/src/SplatView.tsx @@ -1,25 +1,26 @@ -import { forwardRef, useImperativeHandle, useRef } from 'react'; +import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'; import type { ViewProps } from 'react-native'; import NativeSplatView, { Commands, type NativeProps, } from './SplatViewNativeComponent'; +import { + normalizeQuality, + type QualityPreset, + type QualitySettings, +} from './quality'; +import type { CameraPose } from './SplatViewNativeComponent'; -export type { - SplatSource, - QualitySettings, - CameraPose, -} from './SplatViewNativeComponent'; -import type { QualitySettings, CameraPose } from './SplatViewNativeComponent'; - -export type QualityPreset = 'low' | 'medium' | 'high' | 'ultra'; +export type { SplatSource, CameraPose } from './SplatViewNativeComponent'; +export type { QualityPreset, QualitySettings } from './quality'; export type SplatViewProps = Omit & ViewProps & { /** * A preset name, or a preset plus overrides: * `quality="medium"` or `quality={{ preset: 'medium', renderScale: 0.8 }}`. - * `high` when omitted. + * `high` when omitted. Out of range values are clamped with a warning in + * development; an unknown preset falls back to `high`. */ quality?: QualityPreset | QualitySettings; }; @@ -41,33 +42,54 @@ export type SplatViewHandle = { startBenchmark: (seconds?: number) => void; }; +/** One string per distinct quality value, so a literal that does not change is not renormalised. */ +function qualityKey(quality: SplatViewProps['quality']): string { + if (quality === undefined) return ''; + if (typeof quality === 'string') return quality; + return [ + quality.preset, + quality.renderScale, + quality.shDegree, + quality.splatBudget, + quality.cullMarginDegrees, + quality.linearBlending, + ].join('|'); +} + /** * A walkable Gaussian splat world. * * The view has to have a size; a `SurfaceView` with no height renders nothing * and reports no error, so give it `flex: 1` or explicit dimensions. + * Children are not laid out; put a HUD in a sibling view. */ const SplatViewComponent = forwardRef( ({ quality, ...props }, ref) => { - const nativeRef = useRef>(null); + const nativeRef = useRef>(null); + + useImperativeHandle( + ref, + () => ({ + setWalkVelocity(forward: number, right: number) { + if (nativeRef.current == null) return; + Commands.setWalkVelocity(nativeRef.current, forward, right); + }, + setCameraPose({ x, y, z, yaw = 0, pitch = 0 }: CameraPose) { + if (nativeRef.current == null) return; + Commands.setCameraPose(nativeRef.current, x, y, z, yaw, pitch); + }, + startBenchmark(seconds = 10) { + if (nativeRef.current == null) return; + Commands.startBenchmark(nativeRef.current, seconds); + }, + }), + [] + ); - useImperativeHandle(ref, () => ({ - setWalkVelocity(forward: number, right: number) { - if (nativeRef.current == null) return; - Commands.setWalkVelocity(nativeRef.current, forward, right); - }, - setCameraPose({ x, y, z, yaw = 0, pitch = 0 }: CameraPose) { - if (nativeRef.current == null) return; - Commands.setCameraPose(nativeRef.current, x, y, z, yaw, pitch); - }, - startBenchmark(seconds = 10) { - if (nativeRef.current == null) return; - Commands.startBenchmark(nativeRef.current, seconds); - }, - })); + const key = qualityKey(quality); + // eslint-disable-next-line react-hooks/exhaustive-deps + const settings = useMemo(() => normalizeQuality(quality), [key]); - const settings = - typeof quality === 'string' ? { preset: quality } : quality; return ; } ); diff --git a/src/SplatViewNativeComponent.ts b/src/SplatViewNativeComponent.ts index 400085e..b118ee9 100644 --- a/src/SplatViewNativeComponent.ts +++ b/src/SplatViewNativeComponent.ts @@ -29,24 +29,17 @@ type EngineReadyEvent = { }; /** - * How much the renderer spends per frame. `preset` picks the engine's - * `RenderQuality.LOW`, `MEDIUM`, `HIGH` or `ULTRA`; every other field - * overrides one value of that preset. Omit a field to keep the preset's. - * The reason behind each preset and its frame times are in the engine's README. + * The fully populated quality struct the wrapper builds with `normalizeQuality`. + * Every field is present; -1 means "keep the preset's value". See `quality.ts`. */ -export type QualitySettings = { - /** `low`, `medium`, `high` (the default) or `ultra`. */ - preset?: string; - /** Fraction of the surface the splats are drawn at, 0.1 to 2. Above 1 supersamples. */ - renderScale?: CodegenTypes.Double; - /** Spherical harmonics degree drawn, 0 to 3, capped by what the world carries. Takes effect on the next frame. */ - shDegree?: CodegenTypes.Int32; - /** Most splats drawn per frame through the level of detail tree; 0 draws them all. Applies to worlds loaded after it is set. */ - splatBudget?: CodegenTypes.Int32; - /** Angular margin around the view kept drawn so a turn never meets an empty edge. */ - cullMarginDegrees?: CodegenTypes.Double; - /** Blend in linear light instead of the encoded space the training used. */ - linearBlending?: boolean; +type NativeQualityStruct = { + preset: string; + renderScale: CodegenTypes.Double; + shDegree: CodegenTypes.Int32; + splatBudget: CodegenTypes.Int32; + cullMarginDegrees: CodegenTypes.Double; + /** -1 unset, 0 false, 1 true; codegen has no optional boolean with a sentinel. */ + linearBlending: CodegenTypes.Int32; }; /** Where the camera stands, in meters, and where it looks, in radians. */ @@ -66,6 +59,14 @@ type FailureEvent = { message: string; }; +type LoadProgressEvent = { + /** `world` or `collider`. */ + kind: string; + bytes: CodegenTypes.Double; + /** -1 when the source does not say how big it is. */ + total: CodegenTypes.Double; +}; + type StatsEvent = { fps: CodegenTypes.Double; frameMs: CodegenTypes.Double; @@ -87,7 +88,7 @@ export interface NativeProps extends ViewProps { collider?: SplatSource; /** Preset plus overrides; `high` with no overrides when omitted. */ - quality?: QualitySettings; + quality?: NativeQualityStruct; /** * Where the camera starts. Applied when it changes and again when the world * and the collider become ready, so it can be set before the world loads. @@ -108,6 +109,8 @@ export interface NativeProps extends ViewProps { onWorldFailed?: CodegenTypes.DirectEventHandler; onColliderReady?: CodegenTypes.DirectEventHandler; onColliderFailed?: CodegenTypes.DirectEventHandler; + /** Bytes copied so far for a source that is not a local file; at most every 100 ms. */ + onLoadProgress?: CodegenTypes.DirectEventHandler; onStats?: CodegenTypes.DirectEventHandler; } @@ -116,13 +119,13 @@ export type SplatViewNativeComponentType = HostComponent; interface NativeCommands { /** Continuous walking in meters per second, for an on screen joystick. */ setWalkVelocity: ( - viewRef: React.ElementRef, + viewRef: React.ComponentRef, forward: CodegenTypes.Double, right: CodegenTypes.Double ) => void; /** Teleport: position in meters, yaw and pitch in radians. */ setCameraPose: ( - viewRef: React.ElementRef, + viewRef: React.ComponentRef, x: CodegenTypes.Double, y: CodegenTypes.Double, z: CodegenTypes.Double, @@ -131,7 +134,7 @@ interface NativeCommands { ) => void; /** A reproducible turn; the frame time distribution lands in logcat under the tag SplatKit. */ startBenchmark: ( - viewRef: React.ElementRef, + viewRef: React.ComponentRef, seconds: CodegenTypes.Double ) => void; } diff --git a/src/__tests__/quality.test.ts b/src/__tests__/quality.test.ts new file mode 100644 index 0000000..35d0005 --- /dev/null +++ b/src/__tests__/quality.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import { normalizeQuality, UNSET } from '../quality'; + +const noWarn = () => {}; + +describe('normalizeQuality', () => { + it('turns a preset name into a struct with every override unset', () => { + expect(normalizeQuality('medium', noWarn)).toEqual({ + preset: 'medium', + renderScale: UNSET, + shDegree: UNSET, + splatBudget: UNSET, + cullMarginDegrees: UNSET, + linearBlending: UNSET, + }); + }); + + it('defaults to high when nothing is given', () => { + expect(normalizeQuality(undefined, noWarn).preset).toBe('high'); + }); + + it('keeps overrides and marks the rest unset', () => { + const q = normalizeQuality( + { preset: 'low', renderScale: 0.8, linearBlending: true }, + noWarn + ); + expect(q).toEqual({ + preset: 'low', + renderScale: 0.8, + shDegree: UNSET, + splatBudget: UNSET, + cullMarginDegrees: UNSET, + linearBlending: 1, + }); + }); + + it('falls back to high and warns on an unknown preset', () => { + const warn = jest.fn(); + // A typo has to type check to reach here, so cast. + const q = normalizeQuality({ preset: 'ulta' as 'ultra' }, warn); + expect(q.preset).toBe('high'); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('ulta')); + }); + + it('clamps out of range values and warns', () => { + const warn = jest.fn(); + const q = normalizeQuality( + { + preset: 'high', + renderScale: 5, + shDegree: 7, + splatBudget: -3, + cullMarginDegrees: 200, + }, + warn + ); + expect(q.renderScale).toBe(2); + expect(q.shDegree).toBe(3); + expect(q.splatBudget).toBe(0); + expect(q.cullMarginDegrees).toBe(90); + expect(warn).toHaveBeenCalledTimes(4); + }); + + it('rounds a fractional shDegree and splatBudget', () => { + const q = normalizeQuality( + { preset: 'high', shDegree: 1.6, splatBudget: 1000.4 }, + noWarn + ); + expect(q.shDegree).toBe(2); + expect(q.splatBudget).toBe(1000); + }); + + it('treats NaN and Infinity as unset and warns', () => { + const warn = jest.fn(); + const q = normalizeQuality( + { preset: 'high', renderScale: NaN, cullMarginDegrees: Infinity }, + warn + ); + expect(q.renderScale).toBe(UNSET); + expect(q.cullMarginDegrees).toBe(UNSET); + expect(warn).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/index.tsx b/src/index.tsx index 754cb5b..9ec8f34 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,3 +7,5 @@ export type { QualityPreset, CameraPose, } from './SplatView'; +export { normalizeQuality, UNSET } from './quality'; +export type { NativeQuality } from './quality'; diff --git a/src/quality.ts b/src/quality.ts new file mode 100644 index 0000000..813ad22 --- /dev/null +++ b/src/quality.ts @@ -0,0 +1,125 @@ +/** + * `quality` as the app writes it, and as the native side reads it. + * + * The native struct is always fully populated: an override the app did not set + * is sent as `UNSET` (-1). Codegen on some platforms zero-fills struct fields + * that are absent, and 0 is a valid render scale and a valid budget, so + * absence cannot mean "keep the preset's". A negative number can. + */ + +declare const __DEV__: boolean; + +export type QualityPreset = 'low' | 'medium' | 'high' | 'ultra'; + +export type QualitySettings = { + /** `low`, `medium`, `high` (the default) or `ultra`. */ + preset?: QualityPreset; + /** Fraction of the surface the splats are drawn at, 0.1 to 2. Above 1 supersamples. */ + renderScale?: number; + /** Spherical harmonics degree drawn, 0 to 3, capped by what the world carries. */ + shDegree?: number; + /** Most splats drawn per frame through the level of detail tree; 0 draws them all. Applies to worlds loaded after it is set. */ + splatBudget?: number; + /** Angular margin around the view kept drawn so a turn never meets an empty edge, 0 to 90. */ + cullMarginDegrees?: number; + /** Blend in linear light instead of the encoded space the training used. */ + linearBlending?: boolean; +}; + +/** What crosses to native. Every field present; `UNSET` keeps the preset's value. */ +export type NativeQuality = { + preset: string; + renderScale: number; + shDegree: number; + splatBudget: number; + cullMarginDegrees: number; + /** -1 unset, 0 false, 1 true. */ + linearBlending: number; +}; + +export const UNSET = -1; + +const PRESETS: readonly QualityPreset[] = ['low', 'medium', 'high', 'ultra']; + +type Warn = (message: string) => void; + +const defaultWarn: Warn = (message) => { + if (__DEV__) console.warn(`[react-native-splatkit] ${message}`); +}; + +function clamped( + name: string, + value: number | undefined, + min: number, + max: number, + integer: boolean, + warn: Warn +): number { + if (value === undefined) return UNSET; + if (!Number.isFinite(value)) { + warn(`quality.${name} is ${value}; ignoring it`); + return UNSET; + } + let v = integer ? Math.round(value) : value; + if (v < min || v > max) { + warn(`quality.${name} ${value} is outside ${min} to ${max}; clamping`); + v = Math.min(max, Math.max(min, v)); + } + return v; +} + +export function normalizeQuality( + quality: QualityPreset | QualitySettings | undefined, + warn: Warn = defaultWarn +): NativeQuality { + const settings: QualitySettings = + quality === undefined + ? {} + : typeof quality === 'string' + ? { preset: quality } + : quality; + + let preset: QualityPreset = 'high'; + if (settings.preset !== undefined) { + if (PRESETS.includes(settings.preset)) { + preset = settings.preset; + } else { + warn(`unknown quality preset '${String(settings.preset)}'; using 'high'`); + } + } + + return { + preset, + renderScale: clamped( + 'renderScale', + settings.renderScale, + 0.1, + 2, + false, + warn + ), + shDegree: clamped('shDegree', settings.shDegree, 0, 3, true, warn), + splatBudget: clamped( + 'splatBudget', + settings.splatBudget, + 0, + Number.MAX_SAFE_INTEGER, + true, + warn + ), + cullMarginDegrees: clamped( + 'cullMarginDegrees', + settings.cullMarginDegrees, + 0, + 90, + false, + warn + ), + linearBlending: + settings.linearBlending === undefined + ? UNSET + : settings.linearBlending + ? 1 + : 0, + }; +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 3c0636a..3b7ea3a 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig", - "exclude": ["example", "lib"] + "exclude": ["example", "lib", "src/__tests__"] } diff --git a/yarn.lock b/yarn.lock index 56e906f..de9cb67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -39,7 +39,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.2, @babel/core@npm:^7.29.0": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.2, @babel/core@npm:^7.29.0": version: 7.29.7 resolution: "@babel/core@npm:7.29.7" dependencies: @@ -76,7 +76,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.29.1, @babel/generator@npm:^7.29.7, @babel/generator@npm:^7.29.8": +"@babel/generator@npm:^7.29.1, @babel/generator@npm:^7.29.7, @babel/generator@npm:^7.29.8, @babel/generator@npm:^7.7.2": version: 7.29.8 resolution: "@babel/generator@npm:7.29.8" dependencies: @@ -290,7 +290,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.29.0, @babel/parser@npm:^7.29.7, @babel/parser@npm:^7.29.8": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.29.0, @babel/parser@npm:^7.29.7, @babel/parser@npm:^7.29.8": version: 7.29.8 resolution: "@babel/parser@npm:7.29.8" dependencies: @@ -513,7 +513,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.29.7": +"@babel/plugin-syntax-jsx@npm:^7.29.7, @babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.29.7 resolution: "@babel/plugin-syntax-jsx@npm:7.29.7" dependencies: @@ -612,7 +612,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.29.7": +"@babel/plugin-syntax-typescript@npm:^7.29.7, @babel/plugin-syntax-typescript@npm:^7.7.2": version: 7.29.7 resolution: "@babel/plugin-syntax-typescript@npm:7.29.7" dependencies: @@ -1529,6 +1529,13 @@ __metadata: languageName: node linkType: hard +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 10c0/6b80ae4cb3db53f486da2dc63b6e190a74c8c3cca16bb2733f234a0b6a9382b09b146488ae08e2b22cf00f6c83e20f3e040a2f7894f05c045c946d6a090b1d52 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": version: 4.10.1 resolution: "@eslint-community/eslint-utils@npm:4.10.1" @@ -1738,13 +1745,68 @@ __metadata: languageName: node linkType: hard -"@istanbuljs/schema@npm:^0.1.2": +"@istanbuljs/schema@npm:^0.1.2, @istanbuljs/schema@npm:^0.1.3": version: 0.1.6 resolution: "@istanbuljs/schema@npm:0.1.6" checksum: 10c0/bb0d370bf3dd454d2f37f1bccb8921e2da99adacef2da56ef47850e25d7a4de69cf639ead8c189755aef38921369024b4afea3535a5c2ac9082b3e1171bcbc3a languageName: node linkType: hard +"@jest/console@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/console@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10c0/7be408781d0a6f657e969cbec13b540c329671819c2f57acfad0dae9dbfe2c9be859f38fe99b35dba9ff1536937dc6ddc69fdcd2794812fa3c647a1619797f6c + languageName: node + linkType: hard + +"@jest/core@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/core@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/reporters": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-changed-files: "npm:^29.7.0" + jest-config: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-resolve-dependencies: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-ansi: "npm:^6.0.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10c0/934f7bf73190f029ac0f96662c85cd276ec460d407baf6b0dbaec2872e157db4d55a7ee0b1c43b18874602f662b37cb973dda469a4e6d88b4e4845b521adeeb2 + languageName: node + linkType: hard + "@jest/create-cache-key-function@npm:^29.7.0": version: 29.7.0 resolution: "@jest/create-cache-key-function@npm:29.7.0" @@ -1766,6 +1828,25 @@ __metadata: languageName: node linkType: hard +"@jest/expect-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect-utils@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + checksum: 10c0/60b79d23a5358dc50d9510d726443316253ecda3a7fb8072e1526b3e0d3b14f066ee112db95699b7a43ad3f0b61b750c72e28a5a1cac361d7a2bb34747fa938a + languageName: node + linkType: hard + +"@jest/expect@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect@npm:29.7.0" + dependencies: + expect: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + checksum: 10c0/b41f193fb697d3ced134349250aed6ccea075e48c4f803159db102b826a4e473397c68c31118259868fd69a5cba70e97e1c26d2c2ff716ca39dc73a2ccec037e + languageName: node + linkType: hard + "@jest/fake-timers@npm:^29.7.0": version: 29.7.0 resolution: "@jest/fake-timers@npm:29.7.0" @@ -1780,6 +1861,55 @@ __metadata: languageName: node linkType: hard +"@jest/globals@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/globals@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + jest-mock: "npm:^29.7.0" + checksum: 10c0/a385c99396878fe6e4460c43bd7bb0a5cc52befb462cc6e7f2a3810f9e7bcce7cdeb51908fd530391ee452dc856c98baa2c5f5fa8a5b30b071d31ef7f6955cea + languageName: node + linkType: hard + +"@jest/reporters@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/reporters@npm:29.7.0" + dependencies: + "@bcoe/v8-coverage": "npm:^0.2.3" + "@jest/console": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + collect-v8-coverage: "npm:^1.0.0" + exit: "npm:^0.1.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + istanbul-lib-coverage: "npm:^3.0.0" + istanbul-lib-instrument: "npm:^6.0.0" + istanbul-lib-report: "npm:^3.0.0" + istanbul-lib-source-maps: "npm:^4.0.0" + istanbul-reports: "npm:^3.1.3" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + slash: "npm:^3.0.0" + string-length: "npm:^4.0.1" + strip-ansi: "npm:^6.0.0" + v8-to-istanbul: "npm:^9.0.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10c0/a754402a799541c6e5aff2c8160562525e2a47e7d568f01ebfc4da66522de39cbb809bbb0a841c7052e4270d79214e70aec3c169e4eae42a03bc1a8a20cb9fa2 + languageName: node + linkType: hard + "@jest/schemas@npm:^29.6.3": version: 29.6.3 resolution: "@jest/schemas@npm:29.6.3" @@ -1789,6 +1919,41 @@ __metadata: languageName: node linkType: hard +"@jest/source-map@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/source-map@npm:29.6.3" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.18" + callsites: "npm:^3.0.0" + graceful-fs: "npm:^4.2.9" + checksum: 10c0/a2f177081830a2e8ad3f2e29e20b63bd40bade294880b595acf2fc09ec74b6a9dd98f126a2baa2bf4941acd89b13a4ade5351b3885c224107083a0059b60a219 + languageName: node + linkType: hard + +"@jest/test-result@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-result@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + collect-v8-coverage: "npm:^1.0.0" + checksum: 10c0/7de54090e54a674ca173470b55dc1afdee994f2d70d185c80236003efd3fa2b753fff51ffcdda8e2890244c411fd2267529d42c4a50a8303755041ee493e6a04 + languageName: node + linkType: hard + +"@jest/test-sequencer@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-sequencer@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10c0/593a8c4272797bb5628984486080cbf57aed09c7cfdc0a634e8c06c38c6bef329c46c0016e84555ee55d1cd1f381518cf1890990ff845524c1123720c8c1481b + languageName: node + linkType: hard + "@jest/transform@npm:^29.7.0": version: 29.7.0 resolution: "@jest/transform@npm:29.7.0" @@ -1870,7 +2035,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -2509,7 +2674,7 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0": +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" checksum: 10c0/3948088654f3eeb45363f1db158354fb013b362dba2a5c2c18c559484d5eb9f6fd85b23d66c0a7c2fcfab7308d0a585b14dadaca6cc8bf89ebfdc7f8f5102fb7 @@ -2534,6 +2699,16 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:^29.5.14": + version: 29.5.14 + resolution: "@types/jest@npm:29.5.14" + dependencies: + expect: "npm:^29.0.0" + pretty-format: "npm:^29.0.0" + checksum: 10c0/18e0712d818890db8a8dab3d91e9ea9f7f19e3f83c2e50b312f557017dc81466207a71f3ed79cf4428e813ba939954fa26ffa0a9a7f153181ba174581b1c2aed + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.15": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -2804,6 +2979,15 @@ __metadata: languageName: node linkType: hard +"ansi-escapes@npm:^4.2.1": + version: 4.3.2 + resolution: "ansi-escapes@npm:4.3.2" + dependencies: + type-fest: "npm:^0.21.3" + checksum: 10c0/da917be01871525a3dfcf925ae2977bc59e8c513d4423368645634bf5d4ceba5401574eb705c1e92b79f7292af5a656f78c5725a4b0e1cec97c4b413705c1d50 + languageName: node + linkType: hard + "ansi-fragments@npm:^0.2.1": version: 0.2.1 resolution: "ansi-fragments@npm:0.2.1" @@ -3418,6 +3602,13 @@ __metadata: languageName: node linkType: hard +"char-regex@npm:^1.0.2": + version: 1.0.2 + resolution: "char-regex@npm:1.0.2" + checksum: 10c0/57a09a86371331e0be35d9083ba429e86c4f4648ecbe27455dbfb343037c16ee6fdc7f6b61f433a57cc5ded5561d71c56a150e018f40c2ffb7bc93a26dae341e + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -3466,6 +3657,13 @@ __metadata: languageName: node linkType: hard +"cjs-module-lexer@npm:^1.0.0": + version: 1.4.3 + resolution: "cjs-module-lexer@npm:1.4.3" + checksum: 10c0/076b3af85adc4d65dbdab1b5b240fe5b45d44fcf0ef9d429044dd94d19be5589376805c44fb2d4b3e684e5fe6a9b7cf3e426476a6507c45283c5fc6ff95240be + languageName: node + linkType: hard + "cli-cursor@npm:^3.1.0": version: 3.1.0 resolution: "cli-cursor@npm:3.1.0" @@ -3522,6 +3720,20 @@ __metadata: languageName: node linkType: hard +"co@npm:^4.6.0": + version: 4.6.0 + resolution: "co@npm:4.6.0" + checksum: 10c0/c0e85ea0ca8bf0a50cbdca82efc5af0301240ca88ebe3644a6ffb8ffe911f34d40f8fbcf8f1d52c5ddd66706abd4d3bfcd64259f1e8e2371d4f47573b0dc8c28 + languageName: node + linkType: hard + +"collect-v8-coverage@npm:^1.0.0": + version: 1.0.3 + resolution: "collect-v8-coverage@npm:1.0.3" + checksum: 10c0/bc62ba251bcce5e3354a8f88fa6442bee56e3e612fec08d4dfcf66179b41ea0bf544b0f78c4ebc0f8050871220af95bb5c5578a6aef346feea155640582f09dc + languageName: node + linkType: hard + "color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -3679,6 +3891,23 @@ __metadata: languageName: node linkType: hard +"create-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "create-jest@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + prompts: "npm:^2.0.1" + bin: + create-jest: bin/create-jest.js + checksum: 10c0/e7e54c280692470d3398f62a6238fd396327e01c6a0757002833f06d00afc62dd7bfe04ff2b9cd145264460e6b4d1eb8386f2925b7e567f97939843b7b0e812f + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -3746,7 +3975,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.4.0, debug@npm:^4.4.3": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.4.0, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -3765,7 +3994,7 @@ __metadata: languageName: node linkType: hard -"dedent@npm:^1.7.2": +"dedent@npm:^1.0.0, dedent@npm:^1.7.2": version: 1.7.2 resolution: "dedent@npm:1.7.2" peerDependencies: @@ -3784,7 +4013,7 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.3.0": +"deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.0": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044 @@ -3865,6 +4094,20 @@ __metadata: languageName: node linkType: hard +"detect-newline@npm:^3.0.0": + version: 3.1.0 + resolution: "detect-newline@npm:3.1.0" + checksum: 10c0/c38cfc8eeb9fda09febb44bcd85e467c970d4e3bf526095394e5a4f18bc26dd0cf6b22c69c1fa9969261521c593836db335c2795218f6d781a512aea2fb8209d + languageName: node + linkType: hard + +"diff-sequences@npm:^29.6.3": + version: 29.6.3 + resolution: "diff-sequences@npm:29.6.3" + checksum: 10c0/32e27ac7dbffdf2fb0eb5a84efd98a9ad084fbabd5ac9abb8757c6770d5320d2acd172830b28c4add29bb873d59420601dfc805ac4064330ce59b1adfd0593b2 + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -3899,6 +4142,13 @@ __metadata: languageName: node linkType: hard +"emittery@npm:^0.13.1": + version: 0.13.1 + resolution: "emittery@npm:0.13.1" + checksum: 10c0/1573d0ae29ab34661b6c63251ff8f5facd24ccf6a823f19417ae8ba8c88ea450325788c67f16c99edec8de4b52ce93a10fe441ece389fd156e88ee7dab9bfa35 + languageName: node + linkType: hard + "emoji-regex@npm:^10.3.0": version: 10.6.0 resolution: "emoji-regex@npm:10.6.0" @@ -4519,6 +4769,26 @@ __metadata: languageName: node linkType: hard +"exit@npm:^0.1.2": + version: 0.1.2 + resolution: "exit@npm:0.1.2" + checksum: 10c0/71d2ad9b36bc25bb8b104b17e830b40a08989be7f7d100b13269aaae7c3784c3e6e1e88a797e9e87523993a25ba27c8958959a554535370672cfb4d824af8989 + languageName: node + linkType: hard + +"expect@npm:^29.0.0, expect@npm:^29.7.0": + version: 29.7.0 + resolution: "expect@npm:29.7.0" + dependencies: + "@jest/expect-utils": "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10c0/2eddeace66e68b8d8ee5f7be57f3014b19770caaf6815c7a08d131821da527fb8c8cb7b3dcd7c883d2d3d8d184206a4268984618032d1e4b16dc8d6596475d41 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.3 resolution: "exponential-backoff@npm:3.1.3" @@ -4650,7 +4920,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^4.1.0": +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" dependencies: @@ -4902,7 +5172,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.4": +"glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -5089,6 +5359,13 @@ __metadata: languageName: node linkType: hard +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0 + languageName: node + linkType: hard + "http-errors@npm:~2.0.1": version: 2.0.1 resolution: "http-errors@npm:2.0.1" @@ -5159,6 +5436,18 @@ __metadata: languageName: node linkType: hard +"import-local@npm:^3.0.2": + version: 3.2.0 + resolution: "import-local@npm:3.2.0" + dependencies: + pkg-dir: "npm:^4.2.0" + resolve-cwd: "npm:^3.0.0" + bin: + import-local-fixture: fixtures/cli.js + checksum: 10c0/94cd6367a672b7e0cb026970c85b76902d2710a64896fa6de93bd5c571dd03b228c5759308959de205083e3b1c61e799f019c9e36ee8e9c523b993e1057f0433 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -5338,6 +5627,13 @@ __metadata: languageName: node linkType: hard +"is-generator-fn@npm:^2.0.0": + version: 2.1.0 + resolution: "is-generator-fn@npm:2.1.0" + checksum: 10c0/2957cab387997a466cd0bf5c1b6047bd21ecb32bdcfd8996b15747aa01002c1c88731802f1b3d34ac99f4f6874b626418bd118658cf39380fe5fff32a3af9c4d + languageName: node + linkType: hard + "is-generator-function@npm:^1.0.10": version: 1.1.2 resolution: "is-generator-function@npm:1.1.2" @@ -5547,7 +5843,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-coverage@npm:^3.2.0": +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b @@ -5567,6 +5863,51 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-instrument@npm:^6.0.0": + version: 6.0.3 + resolution: "istanbul-lib-instrument@npm:6.0.3" + dependencies: + "@babel/core": "npm:^7.23.9" + "@babel/parser": "npm:^7.23.9" + "@istanbuljs/schema": "npm:^0.1.3" + istanbul-lib-coverage: "npm:^3.2.0" + semver: "npm:^7.5.4" + checksum: 10c0/a1894e060dd2a3b9f046ffdc87b44c00a35516f5e6b7baf4910369acca79e506fc5323a816f811ae23d82334b38e3ddeb8b3b331bd2c860540793b59a8689128 + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: "npm:^3.0.0" + make-dir: "npm:^4.0.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/84323afb14392de8b6a5714bd7e9af845cfbd56cfe71ed276cda2f5f1201aea673c7111901227ee33e68e4364e288d73861eb2ed48f6679d1e69a43b6d9b3ba7 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^4.0.0": + version: 4.0.1 + resolution: "istanbul-lib-source-maps@npm:4.0.1" + dependencies: + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + source-map: "npm:^0.6.1" + checksum: 10c0/19e4cc405016f2c906dff271a76715b3e881fa9faeb3f09a86cb99b8512b3a5ed19cadfe0b54c17ca0e54c1142c9c6de9330d65506e35873994e06634eebeb66 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.3": + version: 3.2.0 + resolution: "istanbul-reports@npm:3.2.0" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10c0/d596317cfd9c22e1394f22a8d8ba0303d2074fe2e971887b32d870e4b33f8464b10f8ccbe6847808f7db485f084eba09e6c2ed706b3a978e4b52f07085b8f9bc + languageName: node + linkType: hard + "iterator.prototype@npm:^1.1.5": version: 1.1.5 resolution: "iterator.prototype@npm:1.1.5" @@ -5581,6 +5922,143 @@ __metadata: languageName: node linkType: hard +"jest-changed-files@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-changed-files@npm:29.7.0" + dependencies: + execa: "npm:^5.0.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + checksum: 10c0/e071384d9e2f6bb462231ac53f29bff86f0e12394c1b49ccafbad225ce2ab7da226279a8a94f421949920bef9be7ef574fd86aee22e8adfa149be73554ab828b + languageName: node + linkType: hard + +"jest-circus@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-circus@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + co: "npm:^4.6.0" + dedent: "npm:^1.0.0" + is-generator-fn: "npm:^2.0.0" + jest-each: "npm:^29.7.0" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + pretty-format: "npm:^29.7.0" + pure-rand: "npm:^6.0.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10c0/8d15344cf7a9f14e926f0deed64ed190c7a4fa1ed1acfcd81e4cc094d3cc5bf7902ebb7b874edc98ada4185688f90c91e1747e0dfd7ac12463b097968ae74b5e + languageName: node + linkType: hard + +"jest-cli@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-cli@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + create-jest: "npm:^29.7.0" + exit: "npm:^0.1.2" + import-local: "npm:^3.0.2" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + yargs: "npm:^17.3.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10c0/a658fd55050d4075d65c1066364595962ead7661711495cfa1dfeecf3d6d0a8ffec532f3dbd8afbb3e172dd5fd2fb2e813c5e10256e7cf2fea766314942fb43a + languageName: node + linkType: hard + +"jest-config@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-config@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/test-sequencer": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-jest: "npm:^29.7.0" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + deepmerge: "npm:^4.2.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-circus: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + parse-json: "npm:^5.2.0" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-json-comments: "npm:^3.1.1" + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + ts-node: + optional: true + checksum: 10c0/bab23c2eda1fff06e0d104b00d6adfb1d1aabb7128441899c9bff2247bd26710b050a5364281ce8d52b46b499153bf7e3ee88b19831a8f3451f1477a0246a0f1 + languageName: node + linkType: hard + +"jest-diff@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-diff@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + diff-sequences: "npm:^29.6.3" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/89a4a7f182590f56f526443dde69acefb1f2f0c9e59253c61d319569856c4931eae66b8a3790c443f529267a0ddba5ba80431c585deed81827032b2b2a1fc999 + languageName: node + linkType: hard + +"jest-docblock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-docblock@npm:29.7.0" + dependencies: + detect-newline: "npm:^3.0.0" + checksum: 10c0/d932a8272345cf6b6142bb70a2bb63e0856cc0093f082821577ea5bdf4643916a98744dfc992189d2b1417c38a11fa42466f6111526bc1fb81366f56410f3be9 + languageName: node + linkType: hard + +"jest-each@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-each@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + pretty-format: "npm:^29.7.0" + checksum: 10c0/f7f9a90ebee80cc688e825feceb2613627826ac41ea76a366fa58e669c3b2403d364c7c0a74d862d469b103c843154f8456d3b1c02b487509a12afa8b59edbb4 + languageName: node + linkType: hard + "jest-environment-node@npm:^29.7.0": version: 29.7.0 resolution: "jest-environment-node@npm:29.7.0" @@ -5625,6 +6103,28 @@ __metadata: languageName: node linkType: hard +"jest-leak-detector@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-leak-detector@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/71bb9f77fc489acb842a5c7be030f2b9acb18574dc9fb98b3100fc57d422b1abc55f08040884bd6e6dbf455047a62f7eaff12aa4058f7cbdc11558718ca6a395 + languageName: node + linkType: hard + +"jest-matcher-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-matcher-utils@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/0d0e70b28fa5c7d4dce701dc1f46ae0922102aadc24ed45d594dd9b7ae0a8a6ef8b216718d1ab79e451291217e05d4d49a82666e1a3cc2b428b75cd9c933244e + languageName: node + linkType: hard + "jest-message-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-message-util@npm:29.7.0" @@ -5653,6 +6153,18 @@ __metadata: languageName: node linkType: hard +"jest-pnp-resolver@npm:^1.2.2": + version: 1.2.3 + resolution: "jest-pnp-resolver@npm:1.2.3" + peerDependencies: + jest-resolve: "*" + peerDependenciesMeta: + jest-resolve: + optional: true + checksum: 10c0/86eec0c78449a2de733a6d3e316d49461af6a858070e113c97f75fb742a48c2396ea94150cbca44159ffd4a959f743a47a8b37a792ef6fdad2cf0a5cba973fac + languageName: node + linkType: hard + "jest-regex-util@npm:^29.6.3": version: 29.6.3 resolution: "jest-regex-util@npm:29.6.3" @@ -5660,6 +6172,120 @@ __metadata: languageName: node linkType: hard +"jest-resolve-dependencies@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve-dependencies@npm:29.7.0" + dependencies: + jest-regex-util: "npm:^29.6.3" + jest-snapshot: "npm:^29.7.0" + checksum: 10c0/b6e9ad8ae5b6049474118ea6441dfddd385b6d1fc471db0136f7c8fbcfe97137a9665e4f837a9f49f15a29a1deb95a14439b7aec812f3f99d08f228464930f0d + languageName: node + linkType: hard + +"jest-resolve@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-pnp-resolver: "npm:^1.2.2" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + resolve: "npm:^1.20.0" + resolve.exports: "npm:^2.0.0" + slash: "npm:^3.0.0" + checksum: 10c0/59da5c9c5b50563e959a45e09e2eace783d7f9ac0b5dcc6375dea4c0db938d2ebda97124c8161310082760e8ebbeff9f6b177c15ca2f57fb424f637a5d2adb47 + languageName: node + linkType: hard + +"jest-runner@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runner@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/environment": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + graceful-fs: "npm:^4.2.9" + jest-docblock: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-leak-detector: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-resolve: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + source-map-support: "npm:0.5.13" + checksum: 10c0/2194b4531068d939f14c8d3274fe5938b77fa73126aedf9c09ec9dec57d13f22c72a3b5af01ac04f5c1cf2e28d0ac0b4a54212a61b05f10b5d6b47f2a1097bb4 + languageName: node + linkType: hard + +"jest-runtime@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runtime@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/globals": "npm:^29.7.0" + "@jest/source-map": "npm:^29.6.3" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + cjs-module-lexer: "npm:^1.0.0" + collect-v8-coverage: "npm:^1.0.0" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-bom: "npm:^4.0.0" + checksum: 10c0/7cd89a1deda0bda7d0941835434e44f9d6b7bd50b5c5d9b0fc9a6c990b2d4d2cab59685ab3cb2850ed4cc37059f6de903af5a50565d7f7f1192a77d3fd6dd2a6 + languageName: node + linkType: hard + +"jest-snapshot@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-snapshot@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@babel/generator": "npm:^7.7.2" + "@babel/plugin-syntax-jsx": "npm:^7.7.2" + "@babel/plugin-syntax-typescript": "npm:^7.7.2" + "@babel/types": "npm:^7.3.3" + "@jest/expect-utils": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + chalk: "npm:^4.0.0" + expect: "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + natural-compare: "npm:^1.4.0" + pretty-format: "npm:^29.7.0" + semver: "npm:^7.5.3" + checksum: 10c0/6e9003c94ec58172b4a62864a91c0146513207bedf4e0a06e1e2ac70a4484088a2683e3a0538d8ea913bcfd53dc54a9b98a98cdfa562e7fe1d1339aeae1da570 + languageName: node + linkType: hard + "jest-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-util@npm:29.7.0" @@ -5688,6 +6314,22 @@ __metadata: languageName: node linkType: hard +"jest-watcher@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-watcher@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + jest-util: "npm:^29.7.0" + string-length: "npm:^4.0.1" + checksum: 10c0/ec6c75030562fc8f8c727cb8f3b94e75d831fc718785abfc196e1f2a2ebc9a2e38744a15147170039628a853d77a3b695561ce850375ede3a4ee6037a2574567 + languageName: node + linkType: hard + "jest-worker@npm:^29.7.0": version: 29.7.0 resolution: "jest-worker@npm:29.7.0" @@ -5700,6 +6342,25 @@ __metadata: languageName: node linkType: hard +"jest@npm:^29.7.0": + version: 29.7.0 + resolution: "jest@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + import-local: "npm:^3.0.2" + jest-cli: "npm:^29.7.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10c0/f40eb8171cf147c617cc6ada49d062fbb03b4da666cb8d39cdbfb739a7d75eea4c3ca150fb072d0d273dce0c753db4d0467d54906ad0293f59c54f9db4a09d8b + languageName: node + linkType: hard + "joi@npm:^17.2.1": version: 17.13.7 resolution: "joi@npm:17.13.7" @@ -5996,6 +6657,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10c0/69b98a6c0b8e5c4fe9acb61608a9fbcfca1756d910f51e5dbe7a9e5cfb74fca9b8a0c8a0ffdf1294a740826c1ab4871d5bf3f62f72a3049e5eac6541ddffed68 + languageName: node + linkType: hard + "makeerror@npm:1.0.12": version: 1.0.12 resolution: "makeerror@npm:1.0.12" @@ -6719,7 +7389,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2": +"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -6861,6 +7531,15 @@ __metadata: languageName: node linkType: hard +"pkg-dir@npm:^4.2.0": + version: 4.2.0 + resolution: "pkg-dir@npm:4.2.0" + dependencies: + find-up: "npm:^4.0.0" + checksum: 10c0/c56bda7769e04907a88423feb320babaed0711af8c436ce3e56763ab1021ba107c7b0cafb11cde7529f669cfc22bffcaebffb573645cbd63842ea9fb17cd7728 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0, possible-typed-array-names@npm:^1.1.0": version: 1.1.0 resolution: "possible-typed-array-names@npm:1.1.0" @@ -6900,7 +7579,7 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.7.0": +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" dependencies: @@ -6927,7 +7606,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.4.2": +"prompts@npm:^2.0.1, prompts@npm:^2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -6955,6 +7634,13 @@ __metadata: languageName: node linkType: hard +"pure-rand@npm:^6.0.0": + version: 6.1.0 + resolution: "pure-rand@npm:6.1.0" + checksum: 10c0/1abe217897bf74dcb3a0c9aba3555fe975023147b48db540aa2faf507aee91c03bf54f6aef0eb2bf59cc259a16d06b28eca37f0dc426d94f4692aeff02fb0e65 + languageName: node + linkType: hard + "qs@npm:~6.15.1": version: 6.15.3 resolution: "qs@npm:6.15.3" @@ -7088,12 +7774,15 @@ __metadata: "@eslint/js": "npm:^10.0.1" "@react-native/babel-preset": "npm:0.85.0" "@react-native/eslint-config": "npm:0.85.0" + "@types/jest": "npm:^29.5.14" "@types/react": "npm:^19.2.0" + babel-jest: "npm:^29.7.0" del-cli: "npm:^7.0.0" eslint: "npm:^9.39.4" eslint-config-prettier: "npm:^10.1.8" eslint-plugin-ft-flow: "npm:^3.0.11" eslint-plugin-prettier: "npm:^5.5.6" + jest: "npm:^29.7.0" prettier: "npm:^3.8.3" react: "npm:19.2.3" react-native: "npm:0.85.0" @@ -7101,8 +7790,8 @@ __metadata: turbo: "npm:^2.9.16" typescript: "npm:^6.0.3" peerDependencies: - react: "*" - react-native: "*" + react: ">=19.0.0" + react-native: ">=0.85.0" languageName: unknown linkType: soft @@ -7281,6 +7970,15 @@ __metadata: languageName: node linkType: hard +"resolve-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "resolve-cwd@npm:3.0.0" + dependencies: + resolve-from: "npm:^5.0.0" + checksum: 10c0/e608a3ebd15356264653c32d7ecbc8fd702f94c6703ea4ac2fb81d9c359180cba0ae2e6b71faa446631ed6145454d5a56b227efc33a2d40638ac13f8beb20ee4 + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -7295,7 +7993,14 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.22.11": +"resolve.exports@npm:^2.0.0": + version: 2.0.3 + resolution: "resolve.exports@npm:2.0.3" + checksum: 10c0/1ade1493f4642a6267d0a5e68faeac20b3d220f18c28b140343feb83694d8fed7a286852aef43689d16042c61e2ddb270be6578ad4a13990769e12065191200d + languageName: node + linkType: hard + +"resolve@npm:^1.20.0, resolve@npm:^1.22.11": version: 1.22.12 resolution: "resolve@npm:1.22.12" dependencies: @@ -7325,7 +8030,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.22.11#optional!builtin": +"resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin": version: 1.22.12 resolution: "resolve@patch:resolve@npm%3A1.22.12#optional!builtin::version=1.22.12&hash=c3c19d" dependencies: @@ -7445,7 +8150,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.7.3": +"semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.7.3": version: 7.8.5 resolution: "semver@npm:7.8.5" bin: @@ -7655,6 +8360,16 @@ __metadata: languageName: node linkType: hard +"source-map-support@npm:0.5.13": + version: 0.5.13 + resolution: "source-map-support@npm:0.5.13" + dependencies: + buffer-from: "npm:^1.0.0" + source-map: "npm:^0.6.0" + checksum: 10c0/137539f8c453fa0f496ea42049ab5da4569f96781f6ac8e5bfda26937be9494f4e8891f523c5f98f0e85f71b35d74127a00c46f83f6a4f54672b58d53202565e + languageName: node + linkType: hard + "source-map-support@npm:~0.5.20": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" @@ -7672,7 +8387,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0": +"source-map@npm:^0.6.0, source-map@npm:^0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 @@ -7735,6 +8450,16 @@ __metadata: languageName: node linkType: hard +"string-length@npm:^4.0.1": + version: 4.0.2 + resolution: "string-length@npm:4.0.2" + dependencies: + char-regex: "npm:^1.0.2" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/1cd77409c3d7db7bc59406f6bcc9ef0783671dcbabb23597a1177c166906ef2ee7c8290f78cae73a8aec858768f189d2cb417797df5e15ec4eb5e16b3346340c + languageName: node + linkType: hard + "string-natural-compare@npm:^3.0.1": version: 3.0.1 resolution: "string-natural-compare@npm:3.0.1" @@ -7880,6 +8605,13 @@ __metadata: languageName: node linkType: hard +"strip-bom@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-bom@npm:4.0.0" + checksum: 10c0/26abad1172d6bc48985ab9a5f96c21e440f6e7e476686de49be813b5a59b3566dccb5c525b831ec54fe348283b47f3ffb8e080bc3f965fde12e84df23f6bb7ef + languageName: node + linkType: hard + "strip-final-newline@npm:^2.0.0": version: 2.0.0 resolution: "strip-final-newline@npm:2.0.0" @@ -8067,6 +8799,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.21.3": + version: 0.21.3 + resolution: "type-fest@npm:0.21.3" + checksum: 10c0/902bd57bfa30d51d4779b641c2bc403cdf1371fb9c91d3c058b0133694fcfdb817aef07a47f40faf79039eecbaa39ee9d3c532deff244f3a19ce68cea71a61e8 + languageName: node + linkType: hard + "type-fest@npm:^0.7.1": version: 0.7.1 resolution: "type-fest@npm:0.7.1" @@ -8279,6 +9018,17 @@ __metadata: languageName: node linkType: hard +"v8-to-istanbul@npm:^9.0.1": + version: 9.3.0 + resolution: "v8-to-istanbul@npm:9.3.0" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.12" + "@types/istanbul-lib-coverage": "npm:^2.0.1" + convert-source-map: "npm:^2.0.0" + checksum: 10c0/968bcf1c7c88c04df1ffb463c179558a2ec17aa49e49376120504958239d9e9dad5281aa05f2a78542b8557f2be0b0b4c325710262f3b838b40d703d5ed30c23 + languageName: node + linkType: hard + "vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -8580,7 +9330,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.6.2": +"yargs@npm:^17.3.1, yargs@npm:^17.6.2": version: 17.7.3 resolution: "yargs@npm:17.7.3" dependencies: From e40ffad724f01adfe4d342dd91bc11309119827a Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:10:51 -0300 Subject: [PATCH 04/14] Map quality in one place: sentinels for unset overrides, fall back on an unknown preset, skip unchanged values --- android/build.gradle | 2 + .../com/splatkit/reactnative/QualityMapper.kt | 50 ++++++++++++++++ .../com/splatkit/reactnative/SplatKitView.kt | 31 ++++------ .../splatkit/reactnative/QualityMapperTest.kt | 58 +++++++++++++++++++ 4 files changed, 122 insertions(+), 19 deletions(-) create mode 100644 android/src/main/java/com/splatkit/reactnative/QualityMapper.kt create mode 100644 android/src/test/java/com/splatkit/reactnative/QualityMapperTest.kt diff --git a/android/build.gradle b/android/build.gradle index f6251e1..ae7675d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -67,4 +67,6 @@ dependencies { // no shaders and no native code of its own; it only puts the engine's view in // a React Native tree. api "io.github.xget7:splatkit-android:${getExtOrDefault('splatkitVersion')}" + + testImplementation "junit:junit:4.13.2" } diff --git a/android/src/main/java/com/splatkit/reactnative/QualityMapper.kt b/android/src/main/java/com/splatkit/reactnative/QualityMapper.kt new file mode 100644 index 0000000..e4f91af --- /dev/null +++ b/android/src/main/java/com/splatkit/reactnative/QualityMapper.kt @@ -0,0 +1,50 @@ +package com.splatkit.reactnative + +import com.facebook.react.bridge.ReadableMap +import com.splatkit.RenderQuality + +/** + * The `quality` prop as the JavaScript wrapper sends it: a preset name and one + * value per override, where a negative number means "keep the preset's". + * Absent keys are treated the same way, so a caller through the interop layer + * that sends only a preset still works. + * + * Never throws. A prop is not a place to crash the app from: an unknown preset + * is reported and `high` is used. + */ +internal object QualityMapper { + fun fromMap(map: ReadableMap?, warn: (String) -> Unit): RenderQuality { + if (map == null) return RenderQuality.HIGH + val name = map.stringOrNull("preset") + val preset = name?.let(RenderQuality::named) ?: run { + if (name != null) warn("unknown quality preset '$name'; using high") + RenderQuality.HIGH + } + return preset.copy( + renderScale = map.floatOr("renderScale", preset.renderScale), + shDegree = map.intOr("shDegree", preset.shDegree), + splatBudget = map.intOr("splatBudget", preset.splatBudget), + cullMarginDegrees = map.floatOr("cullMarginDegrees", preset.cullMarginDegrees), + linearBlending = when (map.intOr("linearBlending", -1)) { + -1 -> preset.linearBlending + 0 -> false + else -> true + }, + ) + } + + private fun ReadableMap.stringOrNull(key: String): String? = + if (hasKey(key) && !isNull(key)) getString(key) else null + + private fun ReadableMap.floatOr(key: String, fallback: Float): Float { + if (!hasKey(key) || isNull(key)) return fallback + val value = getDouble(key) + return if (value < 0) fallback else value.toFloat() + } + + private fun ReadableMap.intOr(key: String, fallback: Int): Int { + if (!hasKey(key) || isNull(key)) return fallback + val value = getInt(key) + return if (value < 0) fallback else value + } +} diff --git a/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt b/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt index 1fd7f1c..ec66235 100644 --- a/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt +++ b/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt @@ -4,6 +4,7 @@ import android.content.ContentResolver import android.net.Uri import android.os.Handler import android.os.Looper +import android.util.Log import android.widget.FrameLayout import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.LifecycleEventListener @@ -217,30 +218,18 @@ class SplatKitView(private val reactContext: ThemedReactContext) : else -> throw IllegalArgumentException("unsupported source") } + private var appliedQuality: RenderQuality? = null + /** A preset plus overrides, or the engine's default when the prop is absent. */ fun setQuality(map: ReadableMap?) { - val presetName = map?.takeIf { it.hasKey("preset") }?.getString("preset") - val preset = when (presetName) { - null -> RenderQuality.HIGH - else -> RenderQuality.named(presetName) - ?: throw IllegalArgumentException("unknown quality preset: $presetName") - } - val quality = if (map == null) preset else preset.copy( - renderScale = map.floatOr("renderScale", preset.renderScale), - shDegree = map.intOr("shDegree", preset.shDegree), - splatBudget = map.intOr("splatBudget", preset.splatBudget), - cullMarginDegrees = map.floatOr("cullMarginDegrees", preset.cullMarginDegrees), - linearBlending = if (map.hasKey("linearBlending")) map.getBoolean("linearBlending") else preset.linearBlending, - ) + val quality = QualityMapper.fromMap(map) { Log.w(TAG, it) } + // A literal object in JSX is a new map on every render; the engine only + // hears about an actual change. + if (quality == appliedQuality) return + appliedQuality = quality surface.applyQuality(quality) } - private fun ReadableMap.floatOr(key: String, fallback: Float) = - if (hasKey(key)) getDouble(key).toFloat() else fallback - - private fun ReadableMap.intOr(key: String, fallback: Int) = - if (hasKey(key)) getInt(key) else fallback - fun setDeclaredPose(pose: CameraPose?) { declaredPose = pose if (pose != null && worldReady) surface.cameraPose = pose @@ -313,6 +302,10 @@ class SplatKitView(private val reactContext: ThemedReactContext) : } override fun onHostDestroy() { /* release() runs when the view is dropped */ } + private companion object { + const val TAG = "SplatKit" + } + private fun emit(name: String, payload: WritableMap) { UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) ?.dispatchEvent(SplatEvent(UIManagerHelper.getSurfaceId(this), id, name, payload)) diff --git a/android/src/test/java/com/splatkit/reactnative/QualityMapperTest.kt b/android/src/test/java/com/splatkit/reactnative/QualityMapperTest.kt new file mode 100644 index 0000000..a6f0dc5 --- /dev/null +++ b/android/src/test/java/com/splatkit/reactnative/QualityMapperTest.kt @@ -0,0 +1,58 @@ +package com.splatkit.reactnative + +import com.facebook.react.bridge.JavaOnlyMap +import com.splatkit.RenderQuality +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class QualityMapperTest { + private val warnings = mutableListOf() + + private fun map(vararg pairs: Pair) = + JavaOnlyMap.of(*pairs.flatMap { listOf(it.first, it.second) }.toTypedArray()) + + @Test + fun `null map is the default preset`() { + assertEquals(RenderQuality.HIGH, QualityMapper.fromMap(null, warnings::add)) + } + + @Test + fun `unset sentinels keep the preset`() { + val q = QualityMapper.fromMap( + map( + "preset" to "medium", "renderScale" to -1.0, "shDegree" to -1, + "splatBudget" to -1, "cullMarginDegrees" to -1.0, "linearBlending" to -1, + ), + warnings::add, + ) + assertEquals(RenderQuality.MEDIUM, q) + assertTrue(warnings.isEmpty()) + } + + @Test + fun `overrides replace the preset's values`() { + val q = QualityMapper.fromMap( + map( + "preset" to "low", "renderScale" to 0.8, "shDegree" to -1, + "splatBudget" to -1, "cullMarginDegrees" to -1.0, "linearBlending" to 1, + ), + warnings::add, + ) + assertEquals(RenderQuality.LOW.copy(renderScale = 0.8f, linearBlending = true), q) + } + + @Test + fun `unknown preset falls back to high and warns instead of throwing`() { + val q = QualityMapper.fromMap(map("preset" to "ulta"), warnings::add) + assertEquals(RenderQuality.HIGH, q) + assertEquals(1, warnings.size) + assertTrue(warnings.first().contains("ulta")) + } + + @Test + fun `missing keys mean unset too`() { + val q = QualityMapper.fromMap(map("preset" to "ultra"), warnings::add) + assertEquals(RenderQuality.ULTRA, q) + } +} From e2def0c0729e17dbe09e95c979647b3fedde889a Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:11:08 -0300 Subject: [PATCH 05/14] Compare the declared pose by value and drop events after release --- .../src/main/java/com/splatkit/reactnative/SplatKitView.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt b/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt index ec66235..ab1ba72 100644 --- a/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt +++ b/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt @@ -59,6 +59,7 @@ class SplatKitView(private val reactContext: ThemedReactContext) : private var statsIntervalMs = 0 private var statsTicking = false private var announcedEngine = false + @Volatile private var released = false private var currentWorldUri: String? = null private var currentColliderUri: String? = null @@ -231,6 +232,9 @@ class SplatKitView(private val reactContext: ThemedReactContext) : } fun setDeclaredPose(pose: CameraPose?) { + // A literal in JSX arrives as a new object on every render; only a + // different pose is a teleport. + if (pose == declaredPose) return declaredPose = pose if (pose != null && worldReady) surface.cameraPose = pose } @@ -282,6 +286,7 @@ class SplatKitView(private val reactContext: ThemedReactContext) : /** Called when React Native drops the view; the engine's resources go with it. */ fun release() { + released = true statsTicking = false running = false main.removeCallbacksAndMessages(null) @@ -307,6 +312,7 @@ class SplatKitView(private val reactContext: ThemedReactContext) : } private fun emit(name: String, payload: WritableMap) { + if (released) return UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) ?.dispatchEvent(SplatEvent(UIManagerHelper.getSurfaceId(this), id, name, payload)) } From 56c9903ecd149351a94c61396157dd074d0cece3 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:12:23 -0300 Subject: [PATCH 06/14] Stream remote, content and asset sources to a cache file and map it; timeouts, cancellation and a progress event --- .../com/splatkit/reactnative/SourceFetcher.kt | 117 ++++++++++++++++++ .../com/splatkit/reactnative/SplatKitView.kt | 72 +++++------ .../splatkit/reactnative/SplatViewManager.kt | 1 + eslint.config.mjs | 2 +- example/src/App.tsx | 9 ++ 5 files changed, 165 insertions(+), 36 deletions(-) create mode 100644 android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt diff --git a/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt b/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt new file mode 100644 index 0000000..2e0ff8d --- /dev/null +++ b/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt @@ -0,0 +1,117 @@ +package com.splatkit.reactnative + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL +import java.security.MessageDigest +import java.util.concurrent.CancellationException + +/** + * Turns a source that is not already a file on disk into one, in the app's + * cache directory, so the engine can map it instead of the binding holding it + * on the Java heap. A world is tens to hundreds of megabytes; a `ByteArray` + * that size is an `OutOfMemoryError` waiting for a phone with less RAM. + * + * The cache is keyed by the URI. A changed file behind the same URI is not + * noticed; the README tells apps to bust it with a query string. + * + * Writes go to a `.part` file and are renamed at the end, so a crash or a + * cancellation never leaves a truncated file behind that looks complete. + */ +internal class SourceFetcher(private val context: Context) { + private val dir = File(context.cacheDir, "splatkit").apply { + mkdirs() + listFiles { f -> f.name.endsWith(".part") }?.forEach { it.delete() } + } + + @Volatile private var connection: HttpURLConnection? = null + + /** Called from any thread; makes a blocked network read fail promptly. */ + fun disconnect() { + connection?.disconnect() + } + + @Throws(IOException::class, InterruptedException::class) + fun fetch(uri: String, cancelled: () -> Boolean, progress: (Long, Long) -> Unit): File { + val target = File(dir, "${sha1(uri)}${extensionOf(uri)}") + if (target.isFile && target.length() > 0) { + progress(target.length(), target.length()) + return target + } + val part = File(dir, "${target.name}.part") + val (stream, total) = open(uri) + try { + stream.use { input -> + FileOutputStream(part).use { output -> + val buffer = ByteArray(256 * 1024) + var copied = 0L + while (true) { + if (cancelled() || Thread.currentThread().isInterrupted) { + throw CancellationException("cancelled: $uri") + } + val n = input.read(buffer) + if (n < 0) break + output.write(buffer, 0, n) + copied += n + progress(copied, total) + } + } + } + if (!part.renameTo(target)) throw IOException("could not move ${part.name} into place") + return target + } catch (e: Throwable) { + part.delete() + throw e + } finally { + connection = null + } + } + + private fun open(uri: String): Pair = when { + uri.startsWith("asset://") -> + context.assets.open(uri.removePrefix("asset://")) to -1L + + uri.startsWith("http://") || uri.startsWith("https://") -> { + val c = (URL(uri).openConnection() as HttpURLConnection).apply { + connectTimeout = 15_000 + readTimeout = 30_000 + instanceFollowRedirects = true + } + connection = c + val code = c.responseCode + if (code !in 200..299) { + c.disconnect() + throw IOException("HTTP $code for $uri") + } + c.inputStream to c.contentLengthLong + } + + uri.startsWith("${ContentResolver.SCHEME_CONTENT}://") -> { + val parsed = Uri.parse(uri) + val stream = context.contentResolver.openInputStream(parsed) + ?: throw IOException("the content provider returned nothing for $uri") + val length = runCatching { + context.contentResolver.openAssetFileDescriptor(parsed, "r")?.use { it.length } ?: -1L + }.getOrDefault(-1L) + stream to length + } + + else -> throw IOException("unsupported source: $uri") + } + + private fun extensionOf(uri: String): String { + val path = Uri.parse(uri).path ?: return "" + val dot = path.lastIndexOf('.') + return if (dot >= 0 && dot > path.lastIndexOf('/')) path.substring(dot) else "" + } + + private fun sha1(text: String): String = + MessageDigest.getInstance("SHA-1").digest(text.toByteArray()) + .joinToString("") { "%02x".format(it) } +} diff --git a/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt b/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt index ab1ba72..2a841ce 100644 --- a/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt +++ b/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt @@ -1,9 +1,9 @@ package com.splatkit.reactnative -import android.content.ContentResolver import android.net.Uri import android.os.Handler import android.os.Looper +import android.os.SystemClock import android.util.Log import android.widget.FrameLayout import com.facebook.react.bridge.Arguments @@ -18,7 +18,7 @@ import com.splatkit.RenderQuality import com.splatkit.SplatStats import com.splatkit.SplatSurfaceView import java.io.File -import java.net.URL +import java.util.concurrent.CancellationException import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicLong @@ -42,6 +42,7 @@ class SplatKitView(private val reactContext: ThemedReactContext) : // File and network reads only. Decoding already runs on the engine's own // loader thread, so this stays free for the next source. private val io = Executors.newSingleThreadExecutor { Thread(it, "SplatKitRnIo") } + private val fetcher = SourceFetcher(reactContext) // A source that arrives while an older one is still being read must win, and // the older result must be dropped rather than replace it. The world and the @@ -149,48 +150,67 @@ class SplatKitView(private val reactContext: ThemedReactContext) : if (uri == currentWorldUri) return currentWorldUri = uri worldReady = false - load(uri, worldGeneration, "topWorldFailed", surface::loadWorld, surface::loadWorld) + load(uri, "world", worldGeneration, "topWorldFailed", surface::loadWorld) } fun setCollider(uri: String?) { if (uri == currentColliderUri) return currentColliderUri = uri - load(uri, colliderGeneration, "topColliderFailed", surface::loadCollider, surface::loadCollider) + load(uri, "collider", colliderGeneration, "topColliderFailed", surface::loadCollider) } /** * A file on disk goes to the engine as a path, which maps it instead of - * copying it through the Java heap. Everything else is read to bytes here. - * Either way the hand over is posted, so it lands after the props of the - * same transaction (quality applies to worlds loaded after it is set). + * copying it through the Java heap. Everything else is streamed to a cache + * file first and then handed over the same way. Either way the hand over is + * posted, so it lands after the props of the same transaction (a budget + * applies to worlds loaded after it is set). */ private fun load( uri: String?, + kind: String, generations: AtomicLong, failureEvent: String, - handFile: (File) -> Unit, - handBytes: (ByteArray) -> Unit, + hand: (File) -> Unit, ) { if (uri.isNullOrEmpty()) return val generation = generations.incrementAndGet() + val stale = { generation != generations.get() } fileOf(uri)?.let { file -> - main.post { if (generation == generations.get()) handFile(file) } + main.post { if (!stale()) hand(file) } return } io.execute { - val bytes = try { - readBytes(uri) + var lastProgressAt = 0L + val file = try { + fetcher.fetch(uri, stale) { bytes, total -> + val now = SystemClock.uptimeMillis() + if (now - lastProgressAt < 100 && bytes != total) return@fetch + lastProgressAt = now + main.post { + if (stale()) return@post + emit("topLoadProgress", Arguments.createMap().apply { + putString("kind", kind) + putDouble("bytes", bytes.toDouble()) + putDouble("total", total.toDouble()) + }) + } + } + } catch (e: CancellationException) { + return@execute + } catch (e: InterruptedException) { + return@execute } catch (e: Exception) { - if (generation != generations.get()) return@execute + if (stale()) return@execute main.post { emit(failureEvent, Arguments.createMap().apply { - putString("message", "${e.javaClass.simpleName}: ${e.message} ($uri)") + putString("message", "${e.javaClass.simpleName}: ${e.message}") }) } return@execute } - if (generation != generations.get()) return@execute - main.post { handBytes(bytes) } + if (stale()) return@execute + main.post { hand(file) } } } @@ -200,25 +220,6 @@ class SplatKitView(private val reactContext: ThemedReactContext) : else -> null } - /** - * Worlds are tens to hundreds of megabytes, so the bytes are read here and - * never serialised through the bridge. - */ - private fun readBytes(uri: String): ByteArray = when { - uri.startsWith("asset://") -> - reactContext.assets.open(uri.removePrefix("asset://")).use { it.readBytes() } - - uri.startsWith("http://") || uri.startsWith("https://") -> - URL(uri).openStream().use { it.readBytes() } - - uri.startsWith("${ContentResolver.SCHEME_CONTENT}://") -> - reactContext.contentResolver.openInputStream(Uri.parse(uri)) - ?.use { it.readBytes() } - ?: throw IllegalArgumentException("the content provider returned nothing") - - else -> throw IllegalArgumentException("unsupported source") - } - private var appliedQuality: RenderQuality? = null /** A preset plus overrides, or the engine's default when the prop is absent. */ @@ -290,6 +291,7 @@ class SplatKitView(private val reactContext: ThemedReactContext) : statsTicking = false running = false main.removeCallbacksAndMessages(null) + fetcher.disconnect() io.shutdownNow() reactContext.removeLifecycleEventListener(this) surface.listener = null diff --git a/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt b/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt index acc1c5a..acfdfa4 100644 --- a/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt +++ b/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt @@ -103,6 +103,7 @@ class SplatViewManager : "topWorldFailed" to mapOf("registrationName" to "onWorldFailed"), "topColliderReady" to mapOf("registrationName" to "onColliderReady"), "topColliderFailed" to mapOf("registrationName" to "onColliderFailed"), + "topLoadProgress" to mapOf("registrationName" to "onLoadProgress"), "topStats" to mapOf("registrationName" to "onStats"), ) diff --git a/eslint.config.mjs b/eslint.config.mjs index 16b00bb..c4103c4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -24,6 +24,6 @@ export default defineConfig([ }, }, { - ignores: ['node_modules/', 'lib/'], + ignores: ['node_modules/', 'lib/', 'android/build/', 'example/android/build/', 'example/android/app/build/'], }, ]); diff --git a/example/src/App.tsx b/example/src/App.tsx index 998e6c7..dbde49e 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -114,6 +114,15 @@ export default function App() { onWorldFailed={(e) => setStatus(`world failed: ${e.nativeEvent.message}`) } + onLoadProgress={(e) => { + const { kind, bytes, total } = e.nativeEvent; + const mb = (bytes / 1048576).toFixed(0); + setStatus( + total > 0 + ? `${kind}: ${mb} of ${(total / 1048576).toFixed(0)} MB` + : `${kind}: ${mb} MB` + ); + }} onColliderReady={() => setStatus((s) => `${s}, walking`)} onColliderFailed={(e) => { // Read the event before the updater runs: by then it has been recycled. From d5b3ced83369102f12e149ea780e6f557cfe82f1 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:13:21 -0300 Subject: [PATCH 07/14] Tell a consumer what to set: minSdk, ABI, Expo, retries, caching, progress, children --- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 9 +++-- README.md | 62 +++++++++++++++++++++++++---- example/README.md | 99 ++++------------------------------------------ 4 files changed, 69 insertions(+), 103 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 45d257b..f646209 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -61,7 +61,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -[INSERT CONTACT METHOD]. +juanieltupa@gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57e9cd4..a3df2e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,11 +83,12 @@ The `package.json` file contains various scripts for common tasks: - `yarn`: setup project by installing dependencies. - `yarn typecheck`: type-check files with TypeScript. - - `yarn lint`: lint files with [ESLint](https://eslint.org/). - - `yarn example start`: start the Metro server for the example app. +- `yarn lint`: lint files with [ESLint](https://eslint.org/). +- `yarn test`: run the Jest tests. +- `yarn example start`: start the Metro server for the example app. - `yarn example android`: run the example app on Android. - `yarn example ios`: run the example app on iOS. - + ### Sending a pull request > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). @@ -97,5 +98,5 @@ When you're sending a pull request: - Prefer small pull requests focused on one change. - Verify that linters and tests are passing. - Review the documentation to make sure it looks good. -- Follow the pull request template when opening a pull request. +- One sentence per line in Markdown, plain dashes. - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. diff --git a/README.md b/README.md index 9252f34..d3933a2 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The bytes never cross the bridge. JavaScript hands over a location and the native side reads it on a background thread, because a world is tens to hundreds of megabytes and serialising that would stall the app for as long as it took. `file://`, `content://`, `asset://` for a file in the app assets, `http://`, `https://`, or an absolute path. -A file on disk goes to the engine as a path and is mapped, not copied through the Java heap; the other schemes are read to bytes first. +A file on disk goes to the engine as a path and is mapped, not copied through the Java heap; the other schemes are streamed to the app's cache directory once and mapped from there, with `onLoadProgress` along the way. ### Props @@ -55,10 +55,10 @@ A file on disk goes to the engine as a path and is mapped, not copied through th |---|---| | `source` | The world. SPZ versions 2 to 4; the format is detected from the bytes. | | `collider` | A GLB mesh. Switches the camera from flying to walking. | -| `quality` | A preset name, `low`, `medium`, `high` (the default) or `ultra`, or a preset plus overrides: `{ preset: 'medium', renderScale: 0.8 }`. The overrides are `renderScale` (0.1 to 2, above 1 supersamples), `shDegree` (0 to 3, the harmonics degree drawn), `splatBudget` (0 draws all), `cullMarginDegrees` and `linearBlending`. The reason behind each preset and its frame times are in the [engine's README](https://github.com/Xget7/splatkit-android/blob/main/packages/splatkit-android/README.md). | +| `quality` | A preset name, `low`, `medium`, `high` (the default) or `ultra`, or a preset plus overrides: `{ preset: 'medium', renderScale: 0.8 }`. The overrides are `renderScale` (0.1 to 2, above 1 supersamples), `shDegree` (0 to 3, rounded, the harmonics degree drawn), `splatBudget` (0 draws all), `cullMarginDegrees` (0 to 90) and `linearBlending`. Out of range values are clamped with a warning in development; an unknown preset falls back to `high`. The reason behind each preset and its frame times are in the [engine's README](https://github.com/Xget7/splatkit-android/blob/main/packages/splatkit-android/README.md). | | `cameraPose` | `{ x, y, z, yaw?, pitch? }`, meters and radians. Applied when it changes and again when the world and the collider become ready, so it can be set before the world loads. When walking the camera settles on the floor under the point. | | `motionEnabled` | The gyroscope drives the look direction. | -| `lookSensitivity`, `walkSensitivity` | Gesture tuning. | +| `lookSensitivity`, `walkSensitivity` | Gesture tuning. Radians per pixel for one finger looking (default 0.004) and meters per pixel for two finger walking (default 0.01). | | `statsInterval` | Milliseconds between `onStats`. 0, the default, turns the event off. | ### Events @@ -66,12 +66,15 @@ A file on disk goes to the engine as a path and is mapped, not copied through th `onEngineReady` fires once with `{ available, gpu }`. When `available` is false the device could not start the renderer and the view stays blank; every other call is a no-op. +`onLoadProgress` gives `{ kind, bytes, total }` at most every 100 ms while a source that is not a local file is being copied; `kind` is `world` or `collider` and `total` is -1 when the server did not say. `onWorldReady` gives `{ splatCount }`, `onWorldFailed` and `onColliderFailed` give `{ message }`, `onColliderReady` takes no payload, and `onStats` gives `{ fps, frameMs, gpuMs, sortMs, splatCount, pose }`, where `pose` is the camera as of the last frame in the shape of `cameraPose`. Read it to save a viewpoint and hand it back later. ### Imperative ```tsx +import { SplatView, type SplatViewHandle } from 'react-native-splatkit'; + const splat = useRef(null); splat.current?.setWalkVelocity(forward, right); // meters per second, for a joystick @@ -79,11 +82,56 @@ splat.current?.setCameraPose({ x: 0, y: 1.5, z: 0 }); // teleport; yaw and pi splat.current?.startBenchmark(10); // a reproducible turn, timings in logcat ``` -## Requirements +## Requirements and setup + +React Native 0.85 or newer with the New Architecture (the default since 0.76); there is no interop layer support. +Android 10 (API 29) and a Vulkan 1.1 device. + +In `android/build.gradle` of the app set the floor the engine needs: + +```groovy +ext { + minSdkVersion = 29 +} +``` + +The library pins 29 itself, so a lower app floor fails at build time with this package's name in the message rather than at runtime. + +The engine ships `arm64-v8a` only. +An app that builds every ABI still installs on an x86_64 emulator and then dies on the first frame, so develop on a physical arm64 device, or keep the emulator from installing it at all with `reactNativeArchitectures=arm64-v8a` in `android/gradle.properties`. + +### Expo + +Works in a development build, not in Expo Go. +Raise the floor with `expo-build-properties`: + +```json +["expo-build-properties", { "android": { "minSdkVersion": 29 } }] +``` + +### Retrying, unloading, caching + +React Native resends a prop only when it changes, so after `onWorldFailed` a retry needs a new `uri` (a query string will do) or a new `key` on the view. +The engine has no unload call yet; setting `source` to `undefined` leaves the current world in place. +Sources that are not local files are copied once to the app's cache directory, keyed by URI, and mapped from there; a changed file behind the same URI is not noticed, so change the URI or clear the app cache. + +### Children + +`SplatView` does not lay out React children. +Put a HUD or a joystick in a sibling view, as the example does. + +## Performance + +Measured on a Xiaomi Mi 9 (Adreno 640), the 500k splat World Labs kitchen, preset `medium`, same session, phone cooled between runs. +The engine's own benchmark reports the numbers; the binding adds nothing to the frame. + +| Host | GPU ms p50 | frame ms | fps | +|---|---|---|---| +| Engine dev app | TBM | TBM | TBM | +| This package, example app | TBM | TBM | TBM | -New architecture only. -Android 10 (API 29) and a Vulkan 1.1 device, `arm64-v8a` only: the engine ships that ABI alone, so an x86_64 emulator installs and then dies on the first frame. -Develop on a physical arm64 device. +A remote world is streamed to disk and mapped, so loading a TBM MB file kept the Java heap under TBM MB. +The engine's numbers per preset and per scene are in [docs/BENCHMARKS.md](https://github.com/Xget7/splatkit-android/blob/main/docs/BENCHMARKS.md). ## iOS diff --git a/example/README.md b/example/README.md index 3e2c3f8..728781a 100644 --- a/example/README.md +++ b/example/README.md @@ -1,97 +1,14 @@ -This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). +# react-native-splatkit example -# Getting Started +Walks a World Labs world with an on screen joystick, switches presets live and shows the engine's stats. -> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding. - -## Step 1: Start Metro - -First, you will need to run **Metro**, the JavaScript build tool for React Native. - -To start the Metro dev server, run the following command from the root of your React Native project: - -```sh -# Using npm -npm start - -# OR using Yarn -yarn start -``` - -## Step 2: Build and run your app - -With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app: - -### Android - -```sh -# Using npm -npm run android - -# OR using Yarn -yarn android -``` - -### iOS - -For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps). - -The first time you create a new project, run the Ruby bundler to install CocoaPods itself: - -```sh -bundle install -``` - -Then, and every time you update your native dependencies, run: +Push a world and its collider into the app's own directory, then run it on a physical arm64 device: ```sh -bundle exec pod install +adb push kitchen.spz /sdcard/Android/data/splatkit.example/files/world.spz +adb push kitchen.glb /sdcard/Android/data/splatkit.example/files/collider.glb +yarn && yarn example android ``` -For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html). - -```sh -# Using npm -npm run ios - -# OR using Yarn -yarn ios -``` - -If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device. - -This is one way to run your app — you can also build it directly from Android Studio or Xcode. - -## Step 3: Modify your app - -Now that you have successfully run the app, let's make changes! - -Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). - -When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: - -- **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS). -- **iOS**: Press R in iOS Simulator. - -## Congratulations! :tada: - -You've successfully run and modified your React Native App. :partying_face: - -### Now what? - -- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). -- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started). - -# Troubleshooting - -If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. - -# Learn More - -To learn more about React Native, take a look at the following resources: - -- [React Native Website](https://reactnative.dev) - learn more about React Native. -- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. -- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. -- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. -- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. +The example resolves the library from the workspace source through the `react-native-splatkit-source` condition in `metro.config.js`, so edits to `src/` show up on reload. +Release builds run with R8 on so the example proves the published engine survives minification. From 2e98f40414efe72a336c865fb6929f2d2d2506b8 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:15:15 -0300 Subject: [PATCH 08/14] Example: a Jest test that mounts the view, so the config earns its keep --- example/Gemfile.lock | 124 + example/ios/Podfile.lock | 2080 +++++++++++++++++ .../SplatkitExample.xcodeproj/project.pbxproj | 23 +- .../contents.xcworkspacedata | 10 + example/ios/SplatkitExample/Info.plist | 3 +- example/jest.config.js | 9 + example/package.json | 8 +- example/src/__tests__/App.test.tsx | 14 + 8 files changed, 2266 insertions(+), 5 deletions(-) create mode 100644 example/Gemfile.lock create mode 100644 example/ios/Podfile.lock create mode 100644 example/ios/SplatkitExample.xcworkspace/contents.xcworkspacedata create mode 100644 example/src/__tests__/App.test.tsx diff --git a/example/Gemfile.lock b/example/Gemfile.lock new file mode 100644 index 0000000..296f33a --- /dev/null +++ b/example/Gemfile.lock @@ -0,0 +1,124 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.9) + activesupport (7.1.6) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + mutex_m + securerandom (>= 0.3) + tzinfo (~> 2.0) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + atomos (0.1.3) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + claide (1.1.0) + cocoapods (1.15.2) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.15.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.15.2) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored2 (3.1.2) + concurrent-ruby (1.3.3) + connection_pool (2.5.5) + drb (2.2.3) + escape (0.0.4) + ethon (0.18.0) + ffi (>= 1.15.0) + logger + ffi (1.17.4) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + httpclient (2.9.0) + mutex_m + i18n (1.14.8) + concurrent-ruby (~> 1.0) + json (3.0.2) + logger (1.7.0) + minitest (5.26.1) + molinillo (0.8.0) + mutex_m (0.3.0) + nanaimo (0.3.0) + nap (1.1.0) + netrc (0.11.0) + nkf (0.3.0) + public_suffix (4.0.7) + rexml (3.4.4) + ruby-macho (2.5.1) + securerandom (0.3.2) + typhoeus (1.6.0) + ethon (>= 0.18.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + xcodeproj (1.25.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (>= 3.3.6, < 4.0) + +PLATFORMS + ruby + +DEPENDENCIES + activesupport (>= 6.1.7.5, != 7.1.0) + benchmark + bigdecimal + cocoapods (>= 1.13, != 1.15.1, != 1.15.0) + concurrent-ruby (< 1.3.4) + logger + mutex_m + nkf + xcodeproj (< 1.26.0) + +RUBY VERSION + ruby 2.7.7p221 + +BUNDLED WITH + 2.1.4 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..6442c14 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,2080 @@ +PODS: + - FBLazyVector (0.85.0) + - hermes-engine (250829098.0.10): + - hermes-engine/Pre-built (= 250829098.0.10) + - hermes-engine/Pre-built (250829098.0.10) + - RCTDeprecation (0.85.0) + - RCTRequired (0.85.0) + - RCTSwiftUI (0.85.0) + - RCTSwiftUIWrapper (0.85.0): + - RCTSwiftUI + - RCTTypeSafety (0.85.0): + - FBLazyVector (= 0.85.0) + - RCTRequired (= 0.85.0) + - React-Core (= 0.85.0) + - React (0.85.0): + - React-Core (= 0.85.0) + - React-Core/DevSupport (= 0.85.0) + - React-Core/RCTWebSocket (= 0.85.0) + - React-RCTActionSheet (= 0.85.0) + - React-RCTAnimation (= 0.85.0) + - React-RCTBlob (= 0.85.0) + - React-RCTImage (= 0.85.0) + - React-RCTLinking (= 0.85.0) + - React-RCTNetwork (= 0.85.0) + - React-RCTSettings (= 0.85.0) + - React-RCTText (= 0.85.0) + - React-RCTVibration (= 0.85.0) + - React-callinvoker (0.85.0) + - React-Core (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.85.0) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core-prebuilt (0.85.0): + - ReactNativeDependencies + - React-Core/CoreModulesHeaders (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/Default (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/DevSupport (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.85.0) + - React-Core/RCTWebSocket (= 0.85.0) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTActionSheetHeaders (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTAnimationHeaders (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTBlobHeaders (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTImageHeaders (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTLinkingHeaders (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTNetworkHeaders (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTSettingsHeaders (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTTextHeaders (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTVibrationHeaders (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTWebSocket (0.85.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.85.0) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-CoreModules (0.85.0): + - RCTTypeSafety (= 0.85.0) + - React-Core-prebuilt + - React-Core/CoreModulesHeaders (= 0.85.0) + - React-debug + - React-jsi (= 0.85.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-NativeModulesApple + - React-RCTBlob + - React-RCTFBReactNativeSpec + - React-RCTImage (= 0.85.0) + - React-runtimeexecutor + - React-utils + - ReactCommon + - ReactNativeDependencies + - React-cxxreact (0.85.0): + - hermes-engine + - React-callinvoker (= 0.85.0) + - React-Core-prebuilt + - React-debug (= 0.85.0) + - React-jsi (= 0.85.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-logger (= 0.85.0) + - React-perflogger (= 0.85.0) + - React-runtimeexecutor + - React-timing (= 0.85.0) + - React-utils + - ReactNativeDependencies + - React-debug (0.85.0) + - React-defaultsnativemodule (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-domnativemodule + - React-Fabric/animated + - React-featureflags + - React-featureflagsnativemodule + - React-idlecallbacksnativemodule + - React-intersectionobservernativemodule + - React-jsi + - React-jsiexecutor + - React-microtasksnativemodule + - React-RCTFBReactNativeSpec + - React-webperformancenativemodule + - ReactNativeDependencies + - Yoga + - React-domnativemodule (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-Fabric + - React-Fabric/bridging + - React-FabricComponents + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-Fabric (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/animated (= 0.85.0) + - React-Fabric/animationbackend (= 0.85.0) + - React-Fabric/animations (= 0.85.0) + - React-Fabric/attributedstring (= 0.85.0) + - React-Fabric/bridging (= 0.85.0) + - React-Fabric/componentregistry (= 0.85.0) + - React-Fabric/componentregistrynative (= 0.85.0) + - React-Fabric/components (= 0.85.0) + - React-Fabric/consistency (= 0.85.0) + - React-Fabric/core (= 0.85.0) + - React-Fabric/dom (= 0.85.0) + - React-Fabric/imagemanager (= 0.85.0) + - React-Fabric/leakchecker (= 0.85.0) + - React-Fabric/mounting (= 0.85.0) + - React-Fabric/observers (= 0.85.0) + - React-Fabric/scheduler (= 0.85.0) + - React-Fabric/telemetry (= 0.85.0) + - React-Fabric/uimanager (= 0.85.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animated (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/animationbackend + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animationbackend (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animations (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/attributedstring (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/bridging (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/componentregistry (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/componentregistrynative (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/components/legacyviewmanagerinterop (= 0.85.0) + - React-Fabric/components/root (= 0.85.0) + - React-Fabric/components/scrollview (= 0.85.0) + - React-Fabric/components/view (= 0.85.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/legacyviewmanagerinterop (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/root (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/scrollview (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/view (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-renderercss + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-Fabric/consistency (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/core (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/dom (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/imagemanager (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/leakchecker (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/mounting (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/observers/events (= 0.85.0) + - React-Fabric/observers/intersection (= 0.85.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/events (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/intersection (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/scheduler (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/animationbackend + - React-Fabric/observers/events + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-performancecdpmetrics + - React-performancetimeline + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/telemetry (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/uimanager (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/uimanager/consistency (= 0.85.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/uimanager/consistency (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-FabricComponents (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components (= 0.85.0) + - React-FabricComponents/textlayoutmanager (= 0.85.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components/inputaccessory (= 0.85.0) + - React-FabricComponents/components/iostextinput (= 0.85.0) + - React-FabricComponents/components/modal (= 0.85.0) + - React-FabricComponents/components/rncore (= 0.85.0) + - React-FabricComponents/components/safeareaview (= 0.85.0) + - React-FabricComponents/components/scrollview (= 0.85.0) + - React-FabricComponents/components/switch (= 0.85.0) + - React-FabricComponents/components/text (= 0.85.0) + - React-FabricComponents/components/textinput (= 0.85.0) + - React-FabricComponents/components/unimplementedview (= 0.85.0) + - React-FabricComponents/components/virtualview (= 0.85.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/inputaccessory (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/iostextinput (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/modal (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/rncore (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/safeareaview (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/scrollview (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/switch (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/text (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/textinput (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/unimplementedview (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/virtualview (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/textlayoutmanager (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricImage (0.85.0): + - hermes-engine + - RCTRequired (= 0.85.0) + - RCTTypeSafety (= 0.85.0) + - React-Core-prebuilt + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsiexecutor (= 0.85.0) + - React-logger + - React-rendererdebug + - React-utils + - ReactCommon + - ReactNativeDependencies + - Yoga + - React-featureflags (0.85.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-featureflagsnativemodule (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-graphics (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-utils + - ReactNativeDependencies + - React-hermes (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact (= 0.85.0) + - React-jsi + - React-jsiexecutor (= 0.85.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-oscompat + - React-perflogger (= 0.85.0) + - React-runtimeexecutor + - ReactNativeDependencies + - React-idlecallbacksnativemodule (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-ImageManager (0.85.0): + - React-Core-prebuilt + - React-Core/Default + - React-debug + - React-Fabric + - React-graphics + - React-rendererdebug + - React-utils + - ReactNativeDependencies + - React-intersectionobservernativemodule (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-Fabric + - React-Fabric/bridging + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-jserrorhandler (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - ReactCommon/turbomodule/bridging + - ReactNativeDependencies + - React-jsi (0.85.0): + - hermes-engine + - React-Core-prebuilt + - ReactNativeDependencies + - React-jsiexecutor (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-jserrorhandler + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsinspector (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork + - React-jsinspectortracing + - React-oscompat + - React-perflogger (= 0.85.0) + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsinspectorcdp (0.85.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-jsinspectornetwork (0.85.0): + - React-Core-prebuilt + - React-jsinspectorcdp + - ReactNativeDependencies + - React-jsinspectortracing (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsinspectornetwork + - React-oscompat + - React-timing + - ReactNativeDependencies + - React-jsitooling (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact (= 0.85.0) + - React-debug + - React-jsi (= 0.85.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsitracing (0.85.0): + - React-jsi + - React-logger (0.85.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-Mapbuffer (0.85.0): + - React-Core-prebuilt + - React-debug + - ReactNativeDependencies + - React-microtasksnativemodule (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-NativeModulesApple (0.85.0): + - hermes-engine + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-runtimeexecutor + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-networking (0.85.0): + - React-Core-prebuilt + - React-jsinspectornetwork + - React-jsinspectortracing + - React-performancetimeline + - React-timing + - ReactNativeDependencies + - React-oscompat (0.85.0) + - React-perflogger (0.85.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-performancecdpmetrics (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-performancetimeline + - React-runtimeexecutor + - React-timing + - ReactNativeDependencies + - React-performancetimeline (0.85.0): + - React-Core-prebuilt + - React-featureflags + - React-jsinspector + - React-jsinspectortracing + - React-perflogger + - React-timing + - ReactNativeDependencies + - React-RCTActionSheet (0.85.0): + - React-Core/RCTActionSheetHeaders (= 0.85.0) + - React-RCTAnimation (0.85.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTAnimationHeaders + - React-debug + - React-featureflags + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTAppDelegate (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-CoreModules + - React-debug + - React-defaultsnativemodule + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsitooling + - React-NativeModulesApple + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RCTImage + - React-RCTNetwork + - React-RCTRuntime + - React-rendererdebug + - React-RuntimeApple + - React-RuntimeCore + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon + - ReactNativeDependencies + - React-RCTBlob (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-Core/RCTBlobHeaders + - React-Core/RCTWebSocket + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - React-RCTNetwork + - ReactCommon + - ReactNativeDependencies + - React-RCTFabric (0.85.0): + - hermes-engine + - RCTSwiftUIWrapper + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-FabricComponents + - React-FabricImage + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-networking + - React-performancecdpmetrics + - React-performancetimeline + - React-RCTAnimation + - React-RCTFBReactNativeSpec + - React-RCTImage + - React-RCTText + - React-rendererconsistency + - React-renderercss + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-RCTFBReactNativeSpec (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec/components (= 0.85.0) + - ReactCommon + - ReactNativeDependencies + - React-RCTFBReactNativeSpec/components (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-NativeModulesApple + - React-rendererdebug + - React-utils + - ReactCommon + - ReactNativeDependencies + - Yoga + - React-RCTImage (0.85.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTImageHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - React-RCTNetwork + - ReactCommon + - ReactNativeDependencies + - React-RCTLinking (0.85.0): + - React-Core/RCTLinkingHeaders (= 0.85.0) + - React-jsi (= 0.85.0) + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactCommon/turbomodule/core (= 0.85.0) + - React-RCTNetwork (0.85.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTNetworkHeaders + - React-debug + - React-featureflags + - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork + - React-NativeModulesApple + - React-networking + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTRuntime (0.85.0): + - hermes-engine + - React-Core + - React-Core-prebuilt + - React-debug + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-RuntimeApple + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-utils + - ReactNativeDependencies + - React-RCTSettings (0.85.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTSettingsHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTText (0.85.0): + - React-Core/RCTTextHeaders (= 0.85.0) + - Yoga + - React-RCTVibration (0.85.0): + - React-Core-prebuilt + - React-Core/RCTVibrationHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-rendererconsistency (0.85.0) + - React-renderercss (0.85.0): + - React-debug + - React-utils + - React-rendererdebug (0.85.0): + - React-Core-prebuilt + - React-debug + - ReactNativeDependencies + - React-RuntimeApple (0.85.0): + - hermes-engine + - React-callinvoker + - React-Core-prebuilt + - React-Core/Default + - React-CoreModules + - React-cxxreact + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-Mapbuffer + - React-NativeModulesApple + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - React-RuntimeCore (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-Fabric + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-performancetimeline + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - React-runtimeexecutor (0.85.0): + - React-Core-prebuilt + - React-debug + - React-featureflags + - React-jsi (= 0.85.0) + - React-utils + - ReactNativeDependencies + - React-RuntimeHermes (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-jsitracing + - React-RuntimeCore + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-runtimescheduler (0.85.0): + - hermes-engine + - React-callinvoker + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - React-jsinspectortracing + - React-performancetimeline + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-timing + - React-utils + - ReactNativeDependencies + - React-timing (0.85.0): + - React-debug + - React-utils (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-debug + - React-jsi (= 0.85.0) + - ReactNativeDependencies + - React-webperformancenativemodule (0.85.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-jsi + - React-jsiexecutor + - React-performancetimeline + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - ReactAppDependencyProvider (0.85.0): + - ReactCodegen + - ReactCodegen (0.85.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-FabricImage + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-RCTAppDelegate + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - ReactCommon (0.85.0): + - React-Core-prebuilt + - ReactCommon/turbomodule (= 0.85.0) + - ReactNativeDependencies + - ReactCommon/turbomodule (0.85.0): + - hermes-engine + - React-callinvoker (= 0.85.0) + - React-Core-prebuilt + - React-cxxreact (= 0.85.0) + - React-jsi (= 0.85.0) + - React-logger (= 0.85.0) + - React-perflogger (= 0.85.0) + - ReactCommon/turbomodule/bridging (= 0.85.0) + - ReactCommon/turbomodule/core (= 0.85.0) + - ReactNativeDependencies + - ReactCommon/turbomodule/bridging (0.85.0): + - hermes-engine + - React-callinvoker (= 0.85.0) + - React-Core-prebuilt + - React-cxxreact (= 0.85.0) + - React-jsi (= 0.85.0) + - React-logger (= 0.85.0) + - React-perflogger (= 0.85.0) + - ReactNativeDependencies + - ReactCommon/turbomodule/core (0.85.0): + - hermes-engine + - React-callinvoker (= 0.85.0) + - React-Core-prebuilt + - React-cxxreact (= 0.85.0) + - React-debug (= 0.85.0) + - React-featureflags (= 0.85.0) + - React-jsi (= 0.85.0) + - React-logger (= 0.85.0) + - React-perflogger (= 0.85.0) + - React-utils (= 0.85.0) + - ReactNativeDependencies + - ReactNativeDependencies (0.85.0) + - Splatkit (0.1.0-alpha.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - Yoga (0.0.0) + +DEPENDENCIES: + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../node_modules/react-native/Libraries/Required`) + - RCTSwiftUI (from `../node_modules/react-native/ReactApple/RCTSwiftUI`) + - RCTSwiftUIWrapper (from `../node_modules/react-native/ReactApple/RCTSwiftUIWrapper`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `../node_modules/react-native/`) + - React-Core-prebuilt (from `../node_modules/react-native/React-Core-prebuilt.podspec`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-intersectionobservernativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver`) + - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) + - React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`) + - React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`) + - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-networking (from `../node_modules/react-native/ReactCommon/react/networking`) + - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancecdpmetrics (from `../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) + - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../node_modules/react-native/React`) + - React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../node_modules/react-native/React/Runtime`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`) + - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - React-webperformancenativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) + - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`) + - ReactCodegen (from `build/generated/ios/ReactCodegen`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - ReactNativeDependencies (from `../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`) + - Splatkit (from `../..`) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + +EXTERNAL SOURCES: + FBLazyVector: + :path: "../node_modules/react-native/Libraries/FBLazyVector" + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-v250829098.0.10 + RCTDeprecation: + :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + RCTRequired: + :path: "../node_modules/react-native/Libraries/Required" + RCTSwiftUI: + :path: "../node_modules/react-native/ReactApple/RCTSwiftUI" + RCTSwiftUIWrapper: + :path: "../node_modules/react-native/ReactApple/RCTSwiftUIWrapper" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../node_modules/react-native/" + React-callinvoker: + :path: "../node_modules/react-native/ReactCommon/callinvoker" + React-Core: + :path: "../node_modules/react-native/" + React-Core-prebuilt: + :podspec: "../node_modules/react-native/React-Core-prebuilt.podspec" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-debug: + :path: "../node_modules/react-native/ReactCommon/react/debug" + React-defaultsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + React-domnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" + React-Fabric: + :path: "../node_modules/react-native/ReactCommon" + React-FabricComponents: + :path: "../node_modules/react-native/ReactCommon" + React-FabricImage: + :path: "../node_modules/react-native/ReactCommon" + React-featureflags: + :path: "../node_modules/react-native/ReactCommon/react/featureflags" + React-featureflagsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + React-graphics: + :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-idlecallbacksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + React-ImageManager: + :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + React-intersectionobservernativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver" + React-jserrorhandler: + :path: "../node_modules/react-native/ReactCommon/jserrorhandler" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsinspectorcdp: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" + React-jsinspectornetwork: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network" + React-jsinspectortracing: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + React-jsitooling: + :path: "../node_modules/react-native/ReactCommon/jsitooling" + React-jsitracing: + :path: "../node_modules/react-native/ReactCommon/hermes/executor/" + React-logger: + :path: "../node_modules/react-native/ReactCommon/logger" + React-Mapbuffer: + :path: "../node_modules/react-native/ReactCommon" + React-microtasksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + React-NativeModulesApple: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-networking: + :path: "../node_modules/react-native/ReactCommon/react/networking" + React-oscompat: + :path: "../node_modules/react-native/ReactCommon/oscompat" + React-perflogger: + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-performancecdpmetrics: + :path: "../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" + React-performancetimeline: + :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTFabric: + :path: "../node_modules/react-native/React" + React-RCTFBReactNativeSpec: + :path: "../node_modules/react-native/React" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTRuntime: + :path: "../node_modules/react-native/React/Runtime" + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + React-rendererconsistency: + :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" + React-renderercss: + :path: "../node_modules/react-native/ReactCommon/react/renderer/css" + React-rendererdebug: + :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" + React-RuntimeApple: + :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + React-RuntimeCore: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimeexecutor: + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + React-RuntimeHermes: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimescheduler: + :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-timing: + :path: "../node_modules/react-native/ReactCommon/react/timing" + React-utils: + :path: "../node_modules/react-native/ReactCommon/react/utils" + React-webperformancenativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" + ReactAppDependencyProvider: + :path: build/generated/ios/ReactAppDependencyProvider + ReactCodegen: + :path: build/generated/ios/ReactCodegen + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + ReactNativeDependencies: + :podspec: "../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec" + Splatkit: + :path: "../.." + Yoga: + :path: "../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + FBLazyVector: c00c20551d40126351a6783c47ce75f5b374851b + hermes-engine: 7cd4b05db99bd78a9a93429fc621f58451c3e206 + RCTDeprecation: 3bb167081b134461cfeb875ff7ae1945f8635257 + RCTRequired: 74839f55d5058a133a0bc4569b0afec750957f64 + RCTSwiftUI: 87a316382f3eab4dd13d2a0d0fd2adcce917361a + RCTSwiftUIWrapper: a31d45fd2891c1e44c1912d9d0c0fac18ed275a0 + RCTTypeSafety: abdf2eaed5501a52f2000de668ccfc60b78c3b27 + React: 1b1536b9099195944034e65b1830f463caaa8390 + React-callinvoker: 6dff6d17d1d6cc8fdf85468a649bafed473c65f5 + React-Core: 00faa4d038298089a1d5a5b21dde8660c4f0820d + React-Core-prebuilt: 536d0af0925322c4d76cc028dddfa635723bebae + React-CoreModules: a17807f849bfd86045b0b9a75ec8c19373b482f6 + React-cxxreact: c7b53ace5827be54048288bce5c55f337c41e95f + React-debug: e1f00fcd2cef58a2897471a6d76a4ef5f5f90c74 + React-defaultsnativemodule: a4354f3bc1a8ef54b0a49de6cb730c8d206c3404 + React-domnativemodule: 18124c1b87708faa392e084ee4dfa600a903db5f + React-Fabric: be4ef16f85b9756a0b6a8b4957569981548d879d + React-FabricComponents: 8b9b88285710dbc17051975a794ca089080df1f8 + React-FabricImage: 3b52d895e2838a0d5ed8404ac6ebfdd6a517ad17 + React-featureflags: 2c85a987e70862d03332dd504840539a915722e3 + React-featureflagsnativemodule: 144f925ea93ea2397ad40acbd9c2487cadffc49d + React-graphics: 122811718b6ad23d49e2f25d59cee8ed87f1875a + React-hermes: 2def7f96a41a78196e65722619aca3d285a19c53 + React-idlecallbacksnativemodule: b2821ddb271d09285d9ee58eeda694e4fef3f036 + React-ImageManager: bf6936d4d0509992a2ceb3dfbd557b130add80ad + React-intersectionobservernativemodule: 6b28c3c069e801275d7c5822bd23a3bbcec16a60 + React-jserrorhandler: 37420717a46c0e5e9f2805cdbb76ab8898f3e5a2 + React-jsi: f4b843b6851635f64e2bc17fd176b33dec120ebc + React-jsiexecutor: eb8bd947675ef44bedf6d8990a23681fc758aff0 + React-jsinspector: 132ce1fa7aece674c656db3dcb9bfb2eaff354ca + React-jsinspectorcdp: fa5507da20ea181af5cf15531711a40ad9965dab + React-jsinspectornetwork: 2a83e372275c71e2fbd153e2cf3d0151e1a7cd8d + React-jsinspectortracing: 2f6cad1182c183f249a7a609de8123986b6c5a27 + React-jsitooling: 3bd2e214f61686c6d89d44baea32e6b8dd3cf425 + React-jsitracing: 6b3a3df0e1b28d65a402c68dc4b79d9abf91b41b + React-logger: ee47d5f3b59a46a006c65038ed5d0b1143e37510 + React-Mapbuffer: 7f8bfbe3fcb2203db4ccb3975414af8cabe4bcd0 + React-microtasksnativemodule: ca1f33f7c98b76d923f550d39631eb4e05fa9aec + React-NativeModulesApple: 9c1f8815ebd72cc1c75587fe588513f6dd9cb708 + React-networking: 8f75f882c6794e91e28b458b5bc1461034098c80 + React-oscompat: 5361d0fa7905ba1c3b3c5e7c464d6be9d2d85f4b + React-perflogger: 44ecaa45852241f80e07c0787c8b65516f5e774f + React-performancecdpmetrics: ef5be4428f221866215bf66ae0ed35d1892fab56 + React-performancetimeline: 1253e6fd3c9ab141f22903099c82b6c0d6fd9cff + React-RCTActionSheet: e3d1db66ef805645e83e6e80f2e21922328a79a7 + React-RCTAnimation: dc39e18331edfa4e4f3631ab83086ce4ba15c4bb + React-RCTAppDelegate: d08cad1065637eecaf347286807ca25d5e966396 + React-RCTBlob: bd5a11e3b206b86ccdcffa9538a5a4bea0acc0dd + React-RCTFabric: efea59a73331fe82c9a79d4b0b96dda3c4d1416b + React-RCTFBReactNativeSpec: 1adfc4557960efe5f233925b55d420e2483dd7df + React-RCTImage: 11407de524bafcc1790394728ca3fe40c6719093 + React-RCTLinking: 707855a5142f65472096a2910044444d390e8c96 + React-RCTNetwork: 8033c7c90b0983dcf994220a9bdeaef428f0b3da + React-RCTRuntime: 722152a3a55a2f89485f5bff358ab62ab1843d99 + React-RCTSettings: cf450b5c44e1d8379b06ac9469c8a81c35ecd7c4 + React-RCTText: 19f706ee0de06dd92945da223d8558d849209e9d + React-RCTVibration: 3383f98add29944aebdcdfa89598b09b1cadf13c + React-rendererconsistency: dd05e33df654b44cc5cdf99532b229d60d469b09 + React-renderercss: 2b6291db12578663d260d24f4c3ece458216a738 + React-rendererdebug: a06085705d5e796b1e40ac0f0e71b0868103e1a3 + React-RuntimeApple: 65fad601d27f99f258cb7b72f2e3b467298ea12c + React-RuntimeCore: f86d3cf1a66ceeed2767273899529c190b64efd4 + React-runtimeexecutor: 4022f2022adc7877b867ecc32bc2ba76f541d94f + React-RuntimeHermes: 003fc52c8419f74cdb7d72b426483131f9f6a09e + React-runtimescheduler: 93fc5f5ab550e39afc342158aa7e7ef324d13565 + React-timing: fca9c61f8a6ef76a4ef286a700ab54d3cdf3c079 + React-utils: 5717eed2e4c96e4d8ad0718c2f63494c06c19e47 + React-webperformancenativemodule: c465a47195d1c6f3cd7eeb84f38389124b62ac73 + ReactAppDependencyProvider: 5787b37b8e2e51dfeab697ec031cc7c4080dcea2 + ReactCodegen: b3184a229afd01e7f8058dd81b805b843caa2bf9 + ReactCommon: fe2a3af8975e63efa60f95fca8c34dc85deee360 + ReactNativeDependencies: d61f0a3d1e1e2173221e8f01effe68efda804f2d + Splatkit: d12cb0ab83752755b4b0b88fd1531ffa20ee642a + Yoga: cc155241ea30670878e270d52d74cf29487c4bbb + +PODFILE CHECKSUM: 324b017977cd3d510d13e7db42d89bccf7bd01e8 + +COCOAPODS: 1.15.2 diff --git a/example/ios/SplatkitExample.xcodeproj/project.pbxproj b/example/ios/SplatkitExample.xcodeproj/project.pbxproj index 762e528..34554b2 100644 --- a/example/ios/SplatkitExample.xcodeproj/project.pbxproj +++ b/example/ios/SplatkitExample.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0C80B921A6F3F58F76C31292 /* libPods-SplatkitExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-SplatkitExample.a */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 4F6DF81E468CD9ECC6EC8092 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ @@ -159,6 +160,7 @@ files = ( 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 4F6DF81E468CD9ECC6EC8092 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -271,7 +273,7 @@ "-ObjC", "-lc++", ); - PRODUCT_BUNDLE_IDENTIFIER = "splatkit.example"; + PRODUCT_BUNDLE_IDENTIFIER = splatkit.example; PRODUCT_NAME = SplatkitExample; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -300,7 +302,7 @@ "-ObjC", "-lc++", ); - PRODUCT_BUNDLE_IDENTIFIER = "splatkit.example"; + PRODUCT_BUNDLE_IDENTIFIER = splatkit.example; PRODUCT_NAME = SplatkitExample; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SWIFT_VERSION = 5.0; @@ -370,6 +372,10 @@ ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", @@ -377,8 +383,13 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; + USE_HERMES = true; }; name = Debug; }; @@ -435,6 +446,10 @@ "\"$(inherited)\"", ); MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", @@ -442,8 +457,12 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; + USE_HERMES = true; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/example/ios/SplatkitExample.xcworkspace/contents.xcworkspacedata b/example/ios/SplatkitExample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..aab51a0 --- /dev/null +++ b/example/ios/SplatkitExample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/SplatkitExample/Info.plist b/example/ios/SplatkitExample/Info.plist index e954ea9..94aaf08 100644 --- a/example/ios/SplatkitExample/Info.plist +++ b/example/ios/SplatkitExample/Info.plist @@ -28,7 +28,6 @@ NSAppTransportSecurity - NSAllowsArbitraryLoads NSAllowsLocalNetworking @@ -36,6 +35,8 @@ NSLocationWhenInUseUsageDescription + RCTNewArchEnabled + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/example/jest.config.js b/example/jest.config.js index 294be30..d35aff6 100644 --- a/example/jest.config.js +++ b/example/jest.config.js @@ -1,3 +1,12 @@ module.exports = { preset: '@react-native/jest-preset', + // The package is resolved from the workspace source, the same way + // metro.config.js does it through the react-native-splatkit-source condition. + // react and react-native are pinned to this app's copies so the source, which + // lives one level up, does not pull a second React from the root. + moduleNameMapper: { + '^react-native-splatkit$': '/../src/index.tsx', + '^react$': '/node_modules/react', + '^react-native$': '/node_modules/react-native', + }, }; diff --git a/example/package.json b/example/package.json index a535ca0..574a1f2 100644 --- a/example/package.json +++ b/example/package.json @@ -7,7 +7,8 @@ "ios": "react-native run-ios", "start": "react-native start", "build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"", - "build:ios": "react-native build-ios --mode Debug" + "build:ios": "react-native build-ios --mode Debug", + "test": "jest" }, "dependencies": { "react": "19.2.3", @@ -25,8 +26,11 @@ "@react-native/metro-config": "0.85.0", "@react-native/typescript-config": "0.85.0", "@types/react": "^19.2.0", + "@types/react-test-renderer": "^19.0.0", + "jest": "^29.7.0", "react-native-builder-bob": "^0.43.0", - "react-native-monorepo-config": "^0.4.0" + "react-native-monorepo-config": "^0.4.0", + "react-test-renderer": "19.2.3" }, "engines": { "node": ">= 22.11.0" diff --git a/example/src/__tests__/App.test.tsx b/example/src/__tests__/App.test.tsx new file mode 100644 index 0000000..46d2c4a --- /dev/null +++ b/example/src/__tests__/App.test.tsx @@ -0,0 +1,14 @@ +import { expect, test } from '@jest/globals'; +import { act, create } from 'react-test-renderer'; +import App from '../App'; + +// The point of this test is not the HUD; it is that a consumer's Jest, with +// the React Native preset, can import the package's build and mount the view. +test('the example renders with the native view mounted', async () => { + let tree!: ReturnType; + await act(async () => { + tree = create(); + }); + const json = JSON.stringify(tree.toJSON()); + expect(json).toContain('"type":"SplatView"'); +}); From 53446ef69aac317afd0e14f862052e6b8dc27137 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:15:30 -0300 Subject: [PATCH 09/14] Run the tests in CI and publish to npm from a v tag --- .github/workflows/ci.yml | 9 ++++++++ .github/workflows/publish.yml | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc48702..c898ed0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,11 @@ jobs: - name: Typecheck files run: yarn typecheck + - name: Test + run: yarn test + + - name: Test example + run: yarn example test build-library: runs-on: ubuntu-latest @@ -104,6 +109,10 @@ jobs: run: | yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" + - name: Android unit tests + if: env.turbo_cache_hit != 1 + run: cd example/android && ./gradlew :react-native-splatkit:testDebugUnitTest --console=plain + build-ios: runs-on: macos-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..0808895 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,39 @@ +name: Publish +on: + push: + tags: + - 'v*' + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Setup + uses: ./.github/actions/setup + + - name: Node for npm + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Check + run: yarn lint && yarn typecheck && yarn test + + - name: Version matches tag + run: | + TAG="${GITHUB_REF_NAME#v}" + VERSION="$(node -p "require('./package.json').version")" + test "$TAG" = "$VERSION" || { echo "tag $TAG does not match package.json $VERSION"; exit 1; } + + - name: Publish + run: npm publish --provenance --access public --tag "$( [[ "$GITHUB_REF_NAME" == *-* ]] && echo alpha || echo latest )" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From d2c9eb17af20b83c29cddcd7739acd956cebf6b4 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:16:50 -0300 Subject: [PATCH 10/14] Announce the iOS stub only once an emitter exists; drop the redundant command override --- .../splatkit/reactnative/SplatViewManager.kt | 7 ---- ios/SplatView.mm | 2 +- yarn.lock | 42 ++++++++++++++++++- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt b/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt index acfdfa4..e1ad2d6 100644 --- a/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt +++ b/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt @@ -1,6 +1,5 @@ package com.splatkit.reactnative -import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.SimpleViewManager @@ -88,12 +87,6 @@ class SplatViewManager : view.startBenchmark(seconds.toFloat()) } - // The delegate routes commands on the new architecture; this keeps the view - // usable through the interop layer as well. - override fun receiveCommand(view: SplatKitView, command: String, args: ReadableArray?) { - delegate.receiveCommand(view, command, args) - } - // Codegen wires these on the new architecture; declaring them keeps the view // working through the interop layer too. override fun getExportedCustomDirectEventTypeConstants(): MutableMap = diff --git a/ios/SplatView.mm b/ios/SplatView.mm index 794cb8c..0201dd0 100644 --- a/ios/SplatView.mm +++ b/ios/SplatView.mm @@ -37,8 +37,8 @@ - (void)didMoveToWindow if (self.window == nil || _announced) { return; } - _announced = YES; if (auto emitter = std::static_pointer_cast(_eventEmitter)) { + _announced = YES; emitter->onEngineReady({.available = false, .gpu = ""}); } } diff --git a/yarn.lock b/yarn.lock index de9cb67..8cb60dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2725,6 +2725,24 @@ __metadata: languageName: node linkType: hard +"@types/react-test-renderer@npm:^19.0.0": + version: 19.3.0 + resolution: "@types/react-test-renderer@npm:19.3.0" + dependencies: + "@types/react": "npm:*" + checksum: 10c0/37f5588f698a057a33c1f4d5fd28a042f3df7f29a3554ddebb7f9c8ecd3e5fba085e5b12d14b30734c30b51959ef17544650b2a9704d629d0af251f126d8ae73 + languageName: node + linkType: hard + +"@types/react@npm:*": + version: 19.3.0 + resolution: "@types/react@npm:19.3.0" + dependencies: + csstype: "npm:^3.2.2" + checksum: 10c0/fbcabf303f935ca9f36ed2dcf81751cf84744098c5a683c8a62a0fa7edd31714b48d5c46b12fd0014f7f2dfe4d9ec6de428a850748eec719a70294e51029551b + languageName: node + linkType: hard + "@types/react@npm:^19.2.0": version: 19.2.18 resolution: "@types/react@npm:19.2.18" @@ -7701,6 +7719,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^19.2.3": + version: 19.3.0 + resolution: "react-is@npm:19.3.0" + checksum: 10c0/99b16fc1009222ec0bbe05286566c865908430778ea6ba90dd418c7a11594c6cbb8db58432735b9203e3c6463e3eef5a399df1317ee2d75fd96b3a88cfaa662e + languageName: node + linkType: hard + "react-native-builder-bob@npm:^0.43.0": version: 0.43.0 resolution: "react-native-builder-bob@npm:0.43.0" @@ -7758,10 +7783,13 @@ __metadata: "@react-native/metro-config": "npm:0.85.0" "@react-native/typescript-config": "npm:0.85.0" "@types/react": "npm:^19.2.0" + "@types/react-test-renderer": "npm:^19.0.0" + jest: "npm:^29.7.0" react: "npm:19.2.3" react-native: "npm:0.85.0" react-native-builder-bob: "npm:^0.43.0" react-native-monorepo-config: "npm:^0.4.0" + react-test-renderer: "npm:19.2.3" languageName: unknown linkType: soft @@ -7853,6 +7881,18 @@ __metadata: languageName: node linkType: hard +"react-test-renderer@npm:19.2.3": + version: 19.2.3 + resolution: "react-test-renderer@npm:19.2.3" + dependencies: + react-is: "npm:^19.2.3" + scheduler: "npm:^0.27.0" + peerDependencies: + react: ^19.2.3 + checksum: 10c0/842b82239dbddbc536083a6260c3e1b0507c02a3400bd05879fc19160468fd0f8ab79fec5dceffa6113b131835cc7621212f8415b46ea5156ab66bbfd7e24297 + languageName: node + linkType: hard + "react@npm:19.2.3": version: 19.2.3 resolution: "react@npm:19.2.3" @@ -8134,7 +8174,7 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:0.27.0": +"scheduler@npm:0.27.0, scheduler@npm:^0.27.0": version: 0.27.0 resolution: "scheduler@npm:0.27.0" checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452 From 3c6f70fda8e3d928c0b6ed741c54f1eb483403ef Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:31:49 -0300 Subject: [PATCH 11/14] Review fixes: unique part files and a length check in the fetcher, loads withdrawn on release and on a cleared prop, quality mapping that never throws, a publish gate that runs the native tests --- .github/workflows/ci.yml | 1 - .github/workflows/publish.yml | 29 +++- android/build.gradle | 5 + .../com/splatkit/reactnative/QualityMapper.kt | 69 +++++--- .../com/splatkit/reactnative/SourceFetcher.kt | 140 ++++++++++----- .../com/splatkit/reactnative/SplatKitView.kt | 26 +-- .../splatkit/reactnative/SplatViewManager.kt | 14 +- .../splatkit/reactnative/QualityMapperTest.kt | 35 ++++ .../splatkit/reactnative/SourceFetcherTest.kt | 164 ++++++++++++++++++ ios/SplatView.mm | 17 +- package.json | 4 +- src/SplatView.tsx | 31 ++-- src/__tests__/quality.test.ts | 44 +++++ src/index.tsx | 2 - src/quality.ts | 31 ++-- 15 files changed, 495 insertions(+), 117 deletions(-) create mode 100644 android/src/test/java/com/splatkit/reactnative/SourceFetcherTest.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c898ed0..829fdf4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,7 +110,6 @@ jobs: yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" - name: Android unit tests - if: env.turbo_cache_hit != 1 run: cd example/android && ./gradlew :react-native-splatkit:testDebugUnitTest --console=plain build-ios: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0808895..c110bf3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,11 +21,23 @@ jobs: - name: Node for npm uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: 22 + node-version-file: .nvmrc registry-url: https://registry.npmjs.org + - name: Install JDK + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: 'zulu' + java-version: '17' + - name: Check - run: yarn lint && yarn typecheck && yarn test + run: yarn lint && yarn typecheck && yarn test && yarn example test + + - name: Android unit tests + run: cd example/android && ./gradlew :react-native-splatkit:testDebugUnitTest --console=plain + + - name: Build + run: yarn prepare - name: Version matches tag run: | @@ -33,7 +45,18 @@ jobs: VERSION="$(node -p "require('./package.json').version")" test "$TAG" = "$VERSION" || { echo "tag $TAG does not match package.json $VERSION"; exit 1; } + - name: Dist tag from the version + run: | + case "$GITHUB_REF_NAME" in + *-alpha*) echo "DIST_TAG=alpha" >> "$GITHUB_ENV" ;; + *-beta*) echo "DIST_TAG=beta" >> "$GITHUB_ENV" ;; + *-rc*) echo "DIST_TAG=next" >> "$GITHUB_ENV" ;; + *) echo "DIST_TAG=latest" >> "$GITHUB_ENV" ;; + esac + + # Trusted publishing (OIDC, no secret) when it is configured on npmjs.com; + # otherwise the NPM_TOKEN secret. Either way the tarball carries provenance. - name: Publish - run: npm publish --provenance --access public --tag "$( [[ "$GITHUB_REF_NAME" == *-* ]] && echo alpha || echo latest )" + run: npm publish --provenance --access public --tag "$DIST_TAG" env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/android/build.gradle b/android/build.gradle index ae7675d..06f81a8 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -54,6 +54,11 @@ android { kotlinOptions { jvmTarget = "17" } + + testOptions { + // android.util.Log in the fetcher; the unit tests run on a plain JVM. + unitTests.returnDefaultValues = true + } } repositories { diff --git a/android/src/main/java/com/splatkit/reactnative/QualityMapper.kt b/android/src/main/java/com/splatkit/reactnative/QualityMapper.kt index e4f91af..a6f9d1b 100644 --- a/android/src/main/java/com/splatkit/reactnative/QualityMapper.kt +++ b/android/src/main/java/com/splatkit/reactnative/QualityMapper.kt @@ -5,46 +5,75 @@ import com.splatkit.RenderQuality /** * The `quality` prop as the JavaScript wrapper sends it: a preset name and one - * value per override, where a negative number means "keep the preset's". - * Absent keys are treated the same way, so a caller through the interop layer - * that sends only a preset still works. + * value per override, where -1 means "keep the preset's". Absent keys are + * treated the same way. * - * Never throws. A prop is not a place to crash the app from: an unknown preset - * is reported and `high` is used. + * Never throws. A prop is not a place to crash the app from: an unknown preset, + * a value of the wrong type or a negative number other than -1 is reported + * through [warn] and the preset's value is used instead. */ internal object QualityMapper { + const val UNSET = -1 + fun fromMap(map: ReadableMap?, warn: (String) -> Unit): RenderQuality { if (map == null) return RenderQuality.HIGH - val name = map.stringOrNull("preset") + val name = map.stringOrNull("preset", warn) val preset = name?.let(RenderQuality::named) ?: run { if (name != null) warn("unknown quality preset '$name'; using high") RenderQuality.HIGH } return preset.copy( - renderScale = map.floatOr("renderScale", preset.renderScale), - shDegree = map.intOr("shDegree", preset.shDegree), - splatBudget = map.intOr("splatBudget", preset.splatBudget), - cullMarginDegrees = map.floatOr("cullMarginDegrees", preset.cullMarginDegrees), - linearBlending = when (map.intOr("linearBlending", -1)) { - -1 -> preset.linearBlending + renderScale = map.floatOr("renderScale", preset.renderScale, warn), + shDegree = map.intOr("shDegree", preset.shDegree, warn), + splatBudget = map.intOr("splatBudget", preset.splatBudget, warn), + cullMarginDegrees = map.floatOr("cullMarginDegrees", preset.cullMarginDegrees, warn), + linearBlending = when (map.intOr("linearBlending", UNSET, warn)) { + UNSET -> preset.linearBlending 0 -> false else -> true }, ) } - private fun ReadableMap.stringOrNull(key: String): String? = - if (hasKey(key) && !isNull(key)) getString(key) else null + private fun ReadableMap.stringOrNull(key: String, warn: (String) -> Unit): String? { + if (!hasKey(key) || isNull(key)) return null + return try { + getString(key) + } catch (e: RuntimeException) { + warn("quality.$key is not a string; ignoring it") + null + } + } - private fun ReadableMap.floatOr(key: String, fallback: Float): Float { + private fun ReadableMap.floatOr(key: String, fallback: Float, warn: (String) -> Unit): Float { if (!hasKey(key) || isNull(key)) return fallback - val value = getDouble(key) - return if (value < 0) fallback else value.toFloat() + val value = try { + getDouble(key) + } catch (e: RuntimeException) { + warn("quality.$key is not a number; ignoring it") + return fallback + } + if (value == UNSET.toDouble()) return fallback + if (value < 0) { + warn("quality.$key $value is negative; ignoring it") + return fallback + } + return value.toFloat() } - private fun ReadableMap.intOr(key: String, fallback: Int): Int { + private fun ReadableMap.intOr(key: String, fallback: Int, warn: (String) -> Unit): Int { if (!hasKey(key) || isNull(key)) return fallback - val value = getInt(key) - return if (value < 0) fallback else value + val value = try { + getInt(key) + } catch (e: RuntimeException) { + warn("quality.$key is not a number; ignoring it") + return fallback + } + if (value == UNSET) return fallback + if (value < 0) { + warn("quality.$key $value is negative; ignoring it") + return fallback + } + return value } } diff --git a/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt b/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt index 2e0ff8d..461a4a8 100644 --- a/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt +++ b/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt @@ -3,6 +3,7 @@ package com.splatkit.reactnative import android.content.ContentResolver import android.content.Context import android.net.Uri +import android.util.Log import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -12,6 +13,9 @@ import java.net.URL import java.security.MessageDigest import java.util.concurrent.CancellationException +/** A stream plus its declared length, -1 when the source does not say. */ +internal typealias Opened = Pair + /** * Turns a source that is not already a file on disk into one, in the app's * cache directory, so the engine can map it instead of the binding holding it @@ -21,38 +25,54 @@ import java.util.concurrent.CancellationException * The cache is keyed by the URI. A changed file behind the same URI is not * noticed; the README tells apps to bust it with a query string. * - * Writes go to a `.part` file and are renamed at the end, so a crash or a - * cancellation never leaves a truncated file behind that looks complete. + * Writes go to a `.part` file unique to this fetch and are renamed at the end, + * after the byte count matched what the source declared, so neither a crash, + * a cancellation nor a connection cut short leaves a file that looks complete. + * + * `http(s)` is handled here with the JDK alone; `asset://` and `content://` + * come through [platform] so the class can be tested on a plain JVM. */ -internal class SourceFetcher(private val context: Context) { - private val dir = File(context.cacheDir, "splatkit").apply { - mkdirs() - listFiles { f -> f.name.endsWith(".part") }?.forEach { it.delete() } - } +internal class SourceFetcher( + cacheRoot: File, + private val platform: (String) -> Opened?, +) { + constructor(context: Context) : this(context.cacheDir, AndroidSources(context)) + + private val dir = File(cacheRoot, "splatkit") @Volatile private var connection: HttpURLConnection? = null + @Volatile private var closed = false + + init { + if (!dir.mkdirs() && !dir.isDirectory) Log.e(TAG, "cannot create the cache directory at $dir") + // Another view of this app may be writing its own .part right now, so + // only what nobody could still be writing is swept. + val stale = System.currentTimeMillis() - STALE_PART_MS + dir.listFiles { f -> f.name.endsWith(".part") && f.lastModified() < stale }?.forEach { it.delete() } + } /** Called from any thread; makes a blocked network read fail promptly. */ fun disconnect() { + closed = true connection?.disconnect() } - @Throws(IOException::class, InterruptedException::class) + @Throws(IOException::class) fun fetch(uri: String, cancelled: () -> Boolean, progress: (Long, Long) -> Unit): File { val target = File(dir, "${sha1(uri)}${extensionOf(uri)}") if (target.isFile && target.length() > 0) { progress(target.length(), target.length()) return target } - val part = File(dir, "${target.name}.part") val (stream, total) = open(uri) + val part = File.createTempFile(target.name, ".part", dir) try { + var copied = 0L stream.use { input -> FileOutputStream(part).use { output -> val buffer = ByteArray(256 * 1024) - var copied = 0L while (true) { - if (cancelled() || Thread.currentThread().isInterrupted) { + if (closed || cancelled() || Thread.currentThread().isInterrupted) { throw CancellationException("cancelled: $uri") } val n = input.read(buffer) @@ -63,55 +83,91 @@ internal class SourceFetcher(private val context: Context) { } } } - if (!part.renameTo(target)) throw IOException("could not move ${part.name} into place") + if (total >= 0 && copied != total) { + throw IOException("truncated: got $copied of $total bytes for $uri") + } + if (!part.renameTo(target)) { + throw IOException("could not move the downloaded file into place at $target for $uri") + } + progress(copied, if (total >= 0) total else copied) return target } catch (e: Throwable) { - part.delete() + if (!part.delete() && part.exists()) Log.w(TAG, "could not delete ${part.name}") throw e } finally { connection = null } } - private fun open(uri: String): Pair = when { - uri.startsWith("asset://") -> - context.assets.open(uri.removePrefix("asset://")) to -1L + private fun open(uri: String): Opened = when { + uri.startsWith("http://") || uri.startsWith("https://") -> openHttp(uri) + else -> platform(uri) ?: throw IOException("unsupported source: $uri") + } - uri.startsWith("http://") || uri.startsWith("https://") -> { - val c = (URL(uri).openConnection() as HttpURLConnection).apply { - connectTimeout = 15_000 - readTimeout = 30_000 - instanceFollowRedirects = true - } - connection = c - val code = c.responseCode - if (code !in 200..299) { - c.disconnect() - throw IOException("HTTP $code for $uri") - } - c.inputStream to c.contentLengthLong + private fun openHttp(uri: String): Opened { + val c = (URL(uri).openConnection() as HttpURLConnection).apply { + connectTimeout = 15_000 + readTimeout = 30_000 + instanceFollowRedirects = true + } + connection = c + if (closed) { + c.disconnect() + throw CancellationException("cancelled: $uri") + } + val code = try { + c.responseCode + } catch (e: IOException) { + connection = null + throw e } + if (code !in 200..299) { + c.disconnect() + connection = null + throw IOException("HTTP $code for $uri") + } + // A transparently decompressed body reports the compressed length, which + // the byte count can never match; treat it as unknown. + val total = if (c.contentEncoding.isNullOrEmpty()) c.contentLengthLong else -1L + return c.inputStream to total + } + + companion object { + private const val TAG = "SplatKit" + private const val STALE_PART_MS = 60 * 60 * 1000L + + /** The extension of the last path segment, before any query or fragment; empty when there is none. */ + fun extensionOf(uri: String): String { + val name = uri.substringBefore('#').substringBefore('?').substringAfterLast('/') + val dot = name.lastIndexOf('.') + return if (dot > 0) name.substring(dot) else "" + } + + fun sha1(text: String): String = + MessageDigest.getInstance("SHA-1").digest(text.toByteArray()) + .joinToString("") { "%02x".format(it) } + } +} + +/** The sources only Android can open: app assets and content providers. */ +internal class AndroidSources(private val context: Context) : (String) -> Opened? { + override fun invoke(uri: String): Opened? = when { + uri.startsWith("asset://") -> + context.assets.open(uri.removePrefix("asset://")) to -1L uri.startsWith("${ContentResolver.SCHEME_CONTENT}://") -> { val parsed = Uri.parse(uri) val stream = context.contentResolver.openInputStream(parsed) ?: throw IOException("the content provider returned nothing for $uri") - val length = runCatching { + val length = try { context.contentResolver.openAssetFileDescriptor(parsed, "r")?.use { it.length } ?: -1L - }.getOrDefault(-1L) + } catch (e: Exception) { + Log.w("SplatKit", "no declared length for $uri", e) + -1L + } stream to length } - else -> throw IOException("unsupported source: $uri") - } - - private fun extensionOf(uri: String): String { - val path = Uri.parse(uri).path ?: return "" - val dot = path.lastIndexOf('.') - return if (dot >= 0 && dot > path.lastIndexOf('/')) path.substring(dot) else "" + else -> null } - - private fun sha1(text: String): String = - MessageDigest.getInstance("SHA-1").digest(text.toByteArray()) - .joinToString("") { "%02x".format(it) } } diff --git a/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt b/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt index 2a841ce..db88063 100644 --- a/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt +++ b/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt @@ -173,11 +173,13 @@ class SplatKitView(private val reactContext: ThemedReactContext) : failureEvent: String, hand: (File) -> Unit, ) { - if (uri.isNullOrEmpty()) return + // Bumped before the empty check so that clearing the prop also withdraws + // a load still in flight. val generation = generations.incrementAndGet() + if (uri.isNullOrEmpty()) return val stale = { generation != generations.get() } fileOf(uri)?.let { file -> - main.post { if (!stale()) hand(file) } + main.post { if (!stale() && !released) hand(file) } return } io.execute { @@ -198,19 +200,17 @@ class SplatKitView(private val reactContext: ThemedReactContext) : } } catch (e: CancellationException) { return@execute - } catch (e: InterruptedException) { - return@execute } catch (e: Exception) { - if (stale()) return@execute + if (stale() || released) return@execute + Log.w(TAG, "$kind failed: $uri", e) main.post { emit(failureEvent, Arguments.createMap().apply { - putString("message", "${e.javaClass.simpleName}: ${e.message}") + putString("message", "${e.javaClass.simpleName}: ${e.message ?: "no detail"} ($uri)") }) } return@execute } - if (stale()) return@execute - main.post { hand(file) } + main.post { if (!stale() && !released) hand(file) } } } @@ -288,6 +288,8 @@ class SplatKitView(private val reactContext: ThemedReactContext) : /** Called when React Native drops the view; the engine's resources go with it. */ fun release() { released = true + worldGeneration.incrementAndGet() + colliderGeneration.incrementAndGet() statsTicking = false running = false main.removeCallbacksAndMessages(null) @@ -315,8 +317,12 @@ class SplatKitView(private val reactContext: ThemedReactContext) : private fun emit(name: String, payload: WritableMap) { if (released) return - UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) - ?.dispatchEvent(SplatEvent(UIManagerHelper.getSurfaceId(this), id, name, payload)) + val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) + if (dispatcher == null) { + Log.w(TAG, "dropped $name: no event dispatcher for view $id") + return + } + dispatcher.dispatchEvent(SplatEvent(UIManagerHelper.getSurfaceId(this), id, name, payload)) } } diff --git a/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt b/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt index e1ad2d6..e87df28 100644 --- a/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt +++ b/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt @@ -103,12 +103,16 @@ class SplatViewManager : companion object { const val NAME = "SplatView" + /** A missing or null coordinate is 0 rather than a crash; the prop is typed, so this only guards the interop path. */ + private fun ReadableMap.floatOrZero(key: String): Float = + if (hasKey(key) && !isNull(key)) getDouble(key).toFloat() else 0f + private fun poseOf(map: ReadableMap) = CameraPose( - x = map.getDouble("x").toFloat(), - y = map.getDouble("y").toFloat(), - z = map.getDouble("z").toFloat(), - yaw = if (map.hasKey("yaw")) map.getDouble("yaw").toFloat() else 0f, - pitch = if (map.hasKey("pitch")) map.getDouble("pitch").toFloat() else 0f, + x = map.floatOrZero("x"), + y = map.floatOrZero("y"), + z = map.floatOrZero("z"), + yaw = map.floatOrZero("yaw"), + pitch = map.floatOrZero("pitch"), ) } } diff --git a/android/src/test/java/com/splatkit/reactnative/QualityMapperTest.kt b/android/src/test/java/com/splatkit/reactnative/QualityMapperTest.kt index a6f0dc5..000fd6a 100644 --- a/android/src/test/java/com/splatkit/reactnative/QualityMapperTest.kt +++ b/android/src/test/java/com/splatkit/reactnative/QualityMapperTest.kt @@ -3,6 +3,8 @@ package com.splatkit.reactnative import com.facebook.react.bridge.JavaOnlyMap import com.splatkit.RenderQuality import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -55,4 +57,37 @@ class QualityMapperTest { val q = QualityMapper.fromMap(map("preset" to "ultra"), warnings::add) assertEquals(RenderQuality.ULTRA, q) } + + @Test + fun `linearBlending 0 turns the preset's blending off`() { + val q = QualityMapper.fromMap(map("preset" to "ultra", "linearBlending" to 0), warnings::add) + assertFalse(q.linearBlending) + assertEquals(RenderQuality.ULTRA.copy(linearBlending = false), q) + assertTrue(warnings.isEmpty()) + } + + @Test + fun `wrong types warn instead of throwing`() { + val q = QualityMapper.fromMap( + map("preset" to 42, "shDegree" to "two", "renderScale" to true, "linearBlending" to "yes"), + warnings::add, + ) + assertEquals(RenderQuality.HIGH, q) + assertEquals(4, warnings.size) + } + + @Test + fun `a negative value other than the sentinel is reported, not absorbed`() { + val q = QualityMapper.fromMap(map("preset" to "high", "renderScale" to -0.5, "shDegree" to -2), warnings::add) + assertEquals(RenderQuality.HIGH, q) + assertEquals(2, warnings.size) + assertNotEquals(RenderQuality.HIGH.copy(renderScale = -0.5f), q) + } + + @Test + fun `an explicit null is unset`() { + val q = QualityMapper.fromMap(map("preset" to "medium").apply { putNull("renderScale") }, warnings::add) + assertEquals(RenderQuality.MEDIUM, q) + assertTrue(warnings.isEmpty()) + } } diff --git a/android/src/test/java/com/splatkit/reactnative/SourceFetcherTest.kt b/android/src/test/java/com/splatkit/reactnative/SourceFetcherTest.kt new file mode 100644 index 0000000..26ff661 --- /dev/null +++ b/android/src/test/java/com/splatkit/reactnative/SourceFetcherTest.kt @@ -0,0 +1,164 @@ +package com.splatkit.reactnative + +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.File +import java.net.ServerSocket +import java.net.Socket +import kotlin.concurrent.thread +import java.util.concurrent.CancellationException +import java.util.concurrent.atomic.AtomicInteger + +class SourceFetcherTest { + @get:Rule val folder = TemporaryFolder() + + private lateinit var server: ServerSocket + private val requests = AtomicInteger(0) + private val body = ByteArray(1_000_000) { (it % 251).toByte() } + + private fun fetcher() = SourceFetcher(folder.root) { null } + private fun url(path: String) = "http://127.0.0.1:${server.localPort}$path" + private fun cacheFiles() = File(folder.root, "splatkit").listFiles()?.map { it.name } ?: emptyList() + + /** + * The smallest HTTP/1.0 server that can answer the four cases the fetcher + * has to survive: a whole body, a truncated one, a 404 and one with no + * declared length. The JDK's own server is not on this module's test + * classpath, which is built against android.jar. + */ + @Before + fun serve() { + server = ServerSocket(0, 50, java.net.InetAddress.getLoopbackAddress()) + thread(isDaemon = true) { + while (!server.isClosed) { + val socket = try { server.accept() } catch (e: IOException) { return@thread } + thread(isDaemon = true) { answer(socket) } + } + } + } + + private fun answer(socket: Socket) = socket.use { s -> + val request = s.getInputStream().bufferedReader().readLine() ?: return@use + val path = request.split(' ')[1] + val out = s.getOutputStream() + fun head(status: String, length: Long?) { + val lengthLine = if (length != null) "Content-Length: $length\r\n" else "" + out.write("HTTP/1.0 $status\r\n${lengthLine}Connection: close\r\n\r\n".toByteArray()) + } + when (path) { + "/world.spz" -> { requests.incrementAndGet(); head("200 OK", body.size.toLong()); out.write(body) } + "/short.spz" -> { head("200 OK", body.size.toLong()); out.write(body, 0, 400_000) } + "/missing.spz" -> head("404 Not Found", 0) + "/unsized.spz" -> { head("200 OK", null); out.write(body, 0, 1234) } + else -> head("404 Not Found", 0) + } + out.flush() + } + + @After + fun stop() = server.close() + + @Test + fun `a whole body lands in the cache with a final progress event`() { + val events = mutableListOf>() + val file = fetcher().fetch(url("/world.spz"), { false }) { b, t -> events += b to t } + assertArrayEquals(body, file.readBytes()) + assertEquals(".spz", file.name.takeLast(4)) + assertEquals(body.size.toLong() to body.size.toLong(), events.last()) + assertEquals(listOf(file.name), cacheFiles()) + } + + @Test + fun `a second fetch of the same uri is served from the cache`() { + val f = fetcher() + val first = f.fetch(url("/world.spz"), { false }) { _, _ -> } + val events = mutableListOf>() + val second = f.fetch(url("/world.spz"), { false }) { b, t -> events += b to t } + assertEquals(first, second) + assertEquals(1, requests.get()) + assertEquals(listOf(body.size.toLong() to body.size.toLong()), events) + } + + @Test + fun `a body shorter than the declared length is not cached`() { + val e = runCatching { fetcher().fetch(url("/short.spz"), { false }) { _, _ -> } }.exceptionOrNull() + assertTrue("expected an IOException, got $e", e is IOException) + assertTrue(e!!.message!!.contains("truncated")) + assertEquals(emptyList(), cacheFiles()) + } + + @Test + fun `a non 2xx answer throws with the code and writes nothing`() { + val e = runCatching { fetcher().fetch(url("/missing.spz"), { false }) { _, _ -> } }.exceptionOrNull() + assertTrue(e is IOException) + assertTrue(e!!.message!!.contains("404")) + assertEquals(emptyList(), cacheFiles()) + } + + @Test + fun `an unknown length still ends with a final progress event`() { + val events = mutableListOf>() + fetcher().fetch(url("/unsized.spz"), { false }) { b, t -> events += b to t } + assertEquals(1234L to 1234L, events.last()) + } + + @Test + fun `cancelling mid stream leaves neither a part nor a target`() { + var calls = 0 + val e = runCatching { + fetcher().fetch(url("/world.spz"), { ++calls > 1 }) { _, _ -> } + }.exceptionOrNull() + assertTrue("expected CancellationException, got $e", e is CancellationException) + assertEquals(emptyList(), cacheFiles()) + } + + @Test + fun `two fetchers over one directory do not share a part file`() { + val a = fetcher() + val b = fetcher() + val fromA = a.fetch(url("/world.spz"), { false }) { _, _ -> } + val fromB = b.fetch(url("/world.spz"), { false }) { _, _ -> } + assertEquals(fromA, fromB) + assertArrayEquals(body, fromB.readBytes()) + } + + @Test + fun `platform sources go through the opener and are cached by uri`() { + val bytes = "glb".toByteArray() + val f = SourceFetcher(folder.root) { uri -> + if (uri.startsWith("asset://")) ByteArrayInputStream(bytes) to -1L else null + } + val file = f.fetch("asset://collider.glb", { false }) { _, _ -> } + assertArrayEquals(bytes, file.readBytes()) + assertEquals(".glb", file.name.takeLast(4)) + val e = runCatching { f.fetch("ftp://x", { false }) { _, _ -> } }.exceptionOrNull() + assertTrue(e is IOException && e.message!!.contains("unsupported")) + } + + @Test + fun `extension comes from the last path segment, ignoring query and fragment`() { + assertEquals(".spz", SourceFetcher.extensionOf("https://h/a/b/world.spz?v=2#x")) + assertEquals(".glb", SourceFetcher.extensionOf("asset://collider.glb")) + assertEquals("", SourceFetcher.extensionOf("content://provider/doc/123")) + assertEquals("", SourceFetcher.extensionOf("https://h/.hidden")) + } + + @Test + fun `a fresh fetcher does not sweep a part file another view is writing`() { + val dir = File(folder.root, "splatkit").apply { mkdirs() } + val live = File(dir, "abc.spz123.part").apply { writeText("x") } + val old = File(dir, "old.spz456.part").apply { writeText("x"); setLastModified(System.currentTimeMillis() - 2 * 60 * 60 * 1000L) } + fetcher() + assertTrue(live.exists()) + assertFalse(old.exists()) + } +} diff --git a/ios/SplatView.mm b/ios/SplatView.mm index 0201dd0..1657064 100644 --- a/ios/SplatView.mm +++ b/ios/SplatView.mm @@ -31,9 +31,10 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } -- (void)didMoveToWindow +// Fabric hands over the emitter and attaches the view to a window in an +// order it does not promise, so whichever happens last announces. +- (void)announceIfReady { - [super didMoveToWindow]; if (self.window == nil || _announced) { return; } @@ -43,6 +44,18 @@ - (void)didMoveToWindow } } +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + [self announceIfReady]; +} + +- (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter +{ + [super updateEventEmitter:eventEmitter]; + [self announceIfReady]; +} + - (void)prepareForRecycle { _announced = NO; diff --git a/package.json b/package.json index cf3d6c7..d2d3d6f 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "!android/gradlew", "!android/gradlew.bat", "!android/local.properties", + "!android/src/test", "!**/__tests__", "!**/__fixtures__", "!**/__mocks__", @@ -65,8 +66,7 @@ "homepage": "https://github.com/Xget7/react-native-splatkit#readme", "publishConfig": { "registry": "https://registry.npmjs.org/", - "access": "public", - "tag": "alpha" + "access": "public" }, "devDependencies": { "@eslint/compat": "^2.1.0", diff --git a/src/SplatView.tsx b/src/SplatView.tsx index 8191048..bbc082d 100644 --- a/src/SplatView.tsx +++ b/src/SplatView.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'; +import { forwardRef, useImperativeHandle, useRef } from 'react'; import type { ViewProps } from 'react-native'; import NativeSplatView, { Commands, @@ -42,20 +42,6 @@ export type SplatViewHandle = { startBenchmark: (seconds?: number) => void; }; -/** One string per distinct quality value, so a literal that does not change is not renormalised. */ -function qualityKey(quality: SplatViewProps['quality']): string { - if (quality === undefined) return ''; - if (typeof quality === 'string') return quality; - return [ - quality.preset, - quality.renderScale, - quality.shDegree, - quality.splatBudget, - quality.cullMarginDegrees, - quality.linearBlending, - ].join('|'); -} - /** * A walkable Gaussian splat world. * @@ -86,11 +72,16 @@ const SplatViewComponent = forwardRef( [] ); - const key = qualityKey(quality); - // eslint-disable-next-line react-hooks/exhaustive-deps - const settings = useMemo(() => normalizeQuality(quality), [key]); - - return ; + // Normalised on every render: it is a handful of comparisons, Fabric diffs + // the struct by value, and the Android side skips a value it already + // applied, so a memo here would only add a dependency list to keep in sync. + return ( + + ); } ); diff --git a/src/__tests__/quality.test.ts b/src/__tests__/quality.test.ts index 35d0005..36d33ab 100644 --- a/src/__tests__/quality.test.ts +++ b/src/__tests__/quality.test.ts @@ -80,4 +80,48 @@ describe('normalizeQuality', () => { expect(q.cullMarginDegrees).toBe(UNSET); expect(warn).toHaveBeenCalledTimes(2); }); + + it('pins the unset sentinel to -1, which QualityMapper.kt hardcodes', () => { + expect(UNSET).toBe(-1); + }); + + it('sends an explicit false as 0, not unset', () => { + const q = normalizeQuality( + { preset: 'ultra', linearBlending: false }, + noWarn + ); + expect(q.linearBlending).toBe(0); + }); + + it('never emits a user value as the unset sentinel', () => { + const q = normalizeQuality( + { + preset: 'high', + renderScale: -1, + shDegree: -1, + splatBudget: -1, + cullMarginDegrees: -1, + }, + noWarn + ); + expect(q.renderScale).toBeGreaterThanOrEqual(0); + expect(q.shDegree).toBeGreaterThanOrEqual(0); + expect(q.splatBudget).toBeGreaterThanOrEqual(0); + expect(q.cullMarginDegrees).toBeGreaterThanOrEqual(0); + }); + + it('treats null like undefined and warns on a number', () => { + const warn = jest.fn(); + expect(normalizeQuality(null as unknown as undefined, warn).preset).toBe( + 'high' + ); + expect(warn).not.toHaveBeenCalled(); + expect(normalizeQuality(5 as unknown as 'high', warn).preset).toBe('high'); + expect(warn).toHaveBeenCalledTimes(1); + }); + + it('caps splatBudget at the 32 bit maximum', () => { + const q = normalizeQuality({ preset: 'high', splatBudget: 1e12 }, noWarn); + expect(q.splatBudget).toBe(2147483647); + }); }); diff --git a/src/index.tsx b/src/index.tsx index 9ec8f34..754cb5b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,5 +7,3 @@ export type { QualityPreset, CameraPose, } from './SplatView'; -export { normalizeQuality, UNSET } from './quality'; -export type { NativeQuality } from './quality'; diff --git a/src/quality.ts b/src/quality.ts index 813ad22..16f4227 100644 --- a/src/quality.ts +++ b/src/quality.ts @@ -7,8 +7,6 @@ * absence cannot mean "keep the preset's". A negative number can. */ -declare const __DEV__: boolean; - export type QualityPreset = 'low' | 'medium' | 'high' | 'ultra'; export type QualitySettings = { @@ -40,11 +38,19 @@ export type NativeQuality = { export const UNSET = -1; const PRESETS: readonly QualityPreset[] = ['low', 'medium', 'high', 'ultra']; +/** The native field is 32 bit; anything larger would saturate silently on the way over. */ +const INT32_MAX = 2147483647; type Warn = (message: string) => void; +// One line per distinct problem for the life of the app, in development and +// in release alike: a clamped value changes what is drawn, and a store build +// with no record of it is the kind of bug nobody can attribute later. +const warned = new Set(); const defaultWarn: Warn = (message) => { - if (__DEV__) console.warn(`[react-native-splatkit] ${message}`); + if (warned.has(message)) return; + warned.add(message); + console.warn(`[react-native-splatkit] ${message}`); }; function clamped( @@ -72,12 +78,17 @@ export function normalizeQuality( quality: QualityPreset | QualitySettings | undefined, warn: Warn = defaultWarn ): NativeQuality { - const settings: QualitySettings = - quality === undefined - ? {} - : typeof quality === 'string' - ? { preset: quality } - : quality; + let settings: QualitySettings; + if (quality == null) { + settings = {}; + } else if (typeof quality === 'string') { + settings = { preset: quality }; + } else if (typeof quality === 'object') { + settings = quality; + } else { + warn(`quality must be a preset name or an object, got ${typeof quality}`); + settings = {}; + } let preset: QualityPreset = 'high'; if (settings.preset !== undefined) { @@ -103,7 +114,7 @@ export function normalizeQuality( 'splatBudget', settings.splatBudget, 0, - Number.MAX_SAFE_INTEGER, + INT32_MAX, true, warn ), From 1cbf4f0da4480eca2091fb4abb26cbe459682682 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:37:34 -0300 Subject: [PATCH 12/14] Drop the example Gemfile.lock: it pinned bundler 2.1.4, which cannot load on the CI runner's Ruby 3.4 --- .gitignore | 1 + example/Gemfile.lock | 124 ------------------------------------------- 2 files changed, 1 insertion(+), 124 deletions(-) delete mode 100644 example/Gemfile.lock diff --git a/.gitignore b/.gitignore index 67f3212..4d08ed3 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ example/ios/Pods # Ruby example/vendor/ +example/Gemfile.lock # node.js # diff --git a/example/Gemfile.lock b/example/Gemfile.lock deleted file mode 100644 index 296f33a..0000000 --- a/example/Gemfile.lock +++ /dev/null @@ -1,124 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.9) - activesupport (7.1.6) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - logger (>= 1.4.2) - minitest (>= 5.1) - mutex_m - securerandom (>= 0.3) - tzinfo (~> 2.0) - addressable (2.9.0) - public_suffix (>= 2.0.2, < 8.0) - algoliasearch (1.27.5) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) - atomos (0.1.3) - base64 (0.3.0) - benchmark (0.5.0) - bigdecimal (4.1.2) - claide (1.1.0) - cocoapods (1.15.2) - addressable (~> 2.8) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.15.2) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 2.1, < 3.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.6.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.8.0) - nap (~> 1.0) - ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.15.2) - activesupport (>= 5.0, < 8) - addressable (~> 2.8) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - public_suffix (~> 4.0) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.5) - cocoapods-downloader (2.1) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.1) - cocoapods-trunk (1.6.0) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.2.0) - colored2 (3.1.2) - concurrent-ruby (1.3.3) - connection_pool (2.5.5) - drb (2.2.3) - escape (0.0.4) - ethon (0.18.0) - ffi (>= 1.15.0) - logger - ffi (1.17.4) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - httpclient (2.9.0) - mutex_m - i18n (1.14.8) - concurrent-ruby (~> 1.0) - json (3.0.2) - logger (1.7.0) - minitest (5.26.1) - molinillo (0.8.0) - mutex_m (0.3.0) - nanaimo (0.3.0) - nap (1.1.0) - netrc (0.11.0) - nkf (0.3.0) - public_suffix (4.0.7) - rexml (3.4.4) - ruby-macho (2.5.1) - securerandom (0.3.2) - typhoeus (1.6.0) - ethon (>= 0.18.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - xcodeproj (1.25.1) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (>= 3.3.6, < 4.0) - -PLATFORMS - ruby - -DEPENDENCIES - activesupport (>= 6.1.7.5, != 7.1.0) - benchmark - bigdecimal - cocoapods (>= 1.13, != 1.15.1, != 1.15.0) - concurrent-ruby (< 1.3.4) - logger - mutex_m - nkf - xcodeproj (< 1.26.0) - -RUBY VERSION - ruby 2.7.7p221 - -BUNDLED WITH - 2.1.4 From 2900e234b5086f082688010f4d65cfff329a65b5 Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 22:46:02 -0300 Subject: [PATCH 13/14] One stats ticker that rests while paused, exported event types, a 512 MB cap on the source cache The ticker was a flag and a fresh runnable per start, so an interval that went through zero and back inside one period left two loops emitting. It is one runnable now, removed before every change and stopped with the engine. The cache kept every world ever fetched; it trims the least recently used past 512 MB on startup and a hit counts as use. Consumers can name their event payloads, the README sample reads from the app's own directory, and the interop claim in the manager comment is gone. --- README.md | 9 ++- .../com/splatkit/reactnative/SourceFetcher.kt | 20 +++++- .../com/splatkit/reactnative/SplatKitView.kt | 62 +++++++++---------- .../splatkit/reactnative/SplatViewManager.kt | 4 +- .../splatkit/reactnative/SourceFetcherTest.kt | 25 ++++++++ src/SplatView.tsx | 10 ++- src/SplatViewNativeComponent.ts | 10 +-- src/index.tsx | 5 ++ 8 files changed, 103 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index d3933a2..a73c583 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ import { SplatView } from 'react-native-splatkit'; console.log(e.nativeEvent.gpu)} onWorldReady={(e) => console.log(e.nativeEvent.splatCount, 'splats')} @@ -114,6 +114,7 @@ Raise the floor with `expo-build-properties`: React Native resends a prop only when it changes, so after `onWorldFailed` a retry needs a new `uri` (a query string will do) or a new `key` on the view. The engine has no unload call yet; setting `source` to `undefined` leaves the current world in place. Sources that are not local files are copied once to the app's cache directory, keyed by URI, and mapped from there; a changed file behind the same URI is not noticed, so change the URI or clear the app cache. +The cache is capped at 512 MB and evicts the least recently used world first, so a handful of worlds stay and a season's worth does not. ### Children @@ -157,6 +158,10 @@ Reading `/sdcard/Download` instead means asking for `READ_EXTERNAL_STORAGE`, whi Everything from the engine logs under the tag `SplatKit`. MIUI hides application logs until `adb shell setprop persist.log.tag.SplatKit V`. +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the development loop, the checks that run on a pull request and the conventions. + ## License MIT. diff --git a/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt b/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt index 461a4a8..b8aec7d 100644 --- a/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt +++ b/android/src/main/java/com/splatkit/reactnative/SourceFetcher.kt @@ -34,9 +34,10 @@ internal typealias Opened = Pair */ internal class SourceFetcher( cacheRoot: File, + private val cacheCapBytes: Long = CACHE_CAP_BYTES, private val platform: (String) -> Opened?, ) { - constructor(context: Context) : this(context.cacheDir, AndroidSources(context)) + constructor(context: Context) : this(context.cacheDir, platform = AndroidSources(context)) private val dir = File(cacheRoot, "splatkit") @@ -49,6 +50,20 @@ internal class SourceFetcher( // only what nobody could still be writing is swept. val stale = System.currentTimeMillis() - STALE_PART_MS dir.listFiles { f -> f.name.endsWith(".part") && f.lastModified() < stale }?.forEach { it.delete() } + trim(cacheCapBytes) + } + + /** + * Keeps the finished files under the cap, oldest use first. A hit touches its + * file, so a world in use stays while one from last month goes. + */ + private fun trim(cap: Long) { + val finished = dir.listFiles { f -> f.isFile && !f.name.endsWith(".part") } ?: return + var kept = 0L + finished.sortedByDescending { it.lastModified() }.forEach { f -> + kept += f.length() + if (kept > cap && !f.delete()) Log.w(TAG, "could not evict ${f.name}") + } } /** Called from any thread; makes a blocked network read fail promptly. */ @@ -61,6 +76,7 @@ internal class SourceFetcher( fun fetch(uri: String, cancelled: () -> Boolean, progress: (Long, Long) -> Unit): File { val target = File(dir, "${sha1(uri)}${extensionOf(uri)}") if (target.isFile && target.length() > 0) { + target.setLastModified(System.currentTimeMillis()) progress(target.length(), target.length()) return target } @@ -135,6 +151,8 @@ internal class SourceFetcher( companion object { private const val TAG = "SplatKit" private const val STALE_PART_MS = 60 * 60 * 1000L + /** Worlds run to a hundred MB or more; a handful is worth keeping, a season's worth is not. */ + private const val CACHE_CAP_BYTES = 512L * 1024 * 1024 /** The extension of the last path segment, before any query or fragment; empty when there is none. */ fun extensionOf(uri: String): String { diff --git a/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt b/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt index db88063..4844283 100644 --- a/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt +++ b/android/src/main/java/com/splatkit/reactnative/SplatKitView.kt @@ -58,7 +58,30 @@ class SplatKitView(private val reactContext: ThemedReactContext) : private var running = false private var statsIntervalMs = 0 - private var statsTicking = false + // The one runnable that may be queued; setting the interval or pausing removes it, + // so two cannot loop at once. + private val statsTick = object : Runnable { + override fun run() { + if (statsIntervalMs <= 0 || !running) return + surface.readStats(stats) + emit("topStats", Arguments.createMap().apply { + putDouble("fps", stats.fps.toDouble()) + putDouble("frameMs", stats.frameMillis.toDouble()) + putDouble("gpuMs", stats.gpuMillis.toDouble()) + putDouble("sortMs", stats.sortMillis.toDouble()) + putInt("splatCount", stats.splatCount) + val pose = surface.cameraPose + putMap("pose", Arguments.createMap().apply { + putDouble("x", pose.x.toDouble()) + putDouble("y", pose.y.toDouble()) + putDouble("z", pose.z.toDouble()) + putDouble("yaw", pose.yaw.toDouble()) + putDouble("pitch", pose.pitch.toDouble()) + }) + }) + main.postDelayed(this, statsIntervalMs.toLong()) + } + } private var announcedEngine = false @Volatile private var released = false @@ -131,6 +154,8 @@ class SplatKitView(private val reactContext: ThemedReactContext) : if (shouldRun == running) return running = shouldRun if (shouldRun) surface.resume() else surface.pause() + // A paused engine has no new stats; the ticker rests with it. + syncStatsTicker() } // React Native does not lay out children of a view it does not manage, and a @@ -251,38 +276,14 @@ class SplatKitView(private val reactContext: ThemedReactContext) : fun setStatsInterval(millis: Int) { statsIntervalMs = millis - if (millis > 0) startStatsTicker() else statsTicking = false + syncStatsTicker() } - private fun startStatsTicker() { - if (statsTicking) return - statsTicking = true - val tick = object : Runnable { - override fun run() { - if (!statsTicking || statsIntervalMs <= 0) { - statsTicking = false - return - } - surface.readStats(stats) - emit("topStats", Arguments.createMap().apply { - putDouble("fps", stats.fps.toDouble()) - putDouble("frameMs", stats.frameMillis.toDouble()) - putDouble("gpuMs", stats.gpuMillis.toDouble()) - putDouble("sortMs", stats.sortMillis.toDouble()) - putInt("splatCount", stats.splatCount) - val pose = surface.cameraPose - putMap("pose", Arguments.createMap().apply { - putDouble("x", pose.x.toDouble()) - putDouble("y", pose.y.toDouble()) - putDouble("z", pose.z.toDouble()) - putDouble("yaw", pose.yaw.toDouble()) - putDouble("pitch", pose.pitch.toDouble()) - }) - }) - main.postDelayed(this, statsIntervalMs.toLong()) - } + private fun syncStatsTicker() { + main.removeCallbacks(statsTick) + if (statsIntervalMs > 0 && running && !released) { + main.postDelayed(statsTick, statsIntervalMs.toLong()) } - main.postDelayed(tick, statsIntervalMs.toLong()) } /** Called when React Native drops the view; the engine's resources go with it. */ @@ -290,7 +291,6 @@ class SplatKitView(private val reactContext: ThemedReactContext) : released = true worldGeneration.incrementAndGet() colliderGeneration.incrementAndGet() - statsTicking = false running = false main.removeCallbacksAndMessages(null) fetcher.disconnect() diff --git a/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt b/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt index e87df28..322e210 100644 --- a/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt +++ b/android/src/main/java/com/splatkit/reactnative/SplatViewManager.kt @@ -87,8 +87,8 @@ class SplatViewManager : view.startBenchmark(seconds.toFloat()) } - // Codegen wires these on the new architecture; declaring them keeps the view - // working through the interop layer too. + // Codegen names the events on the JavaScript side; the view manager registry + // still asks for them here, and an unnamed direct event is silently dropped. override fun getExportedCustomDirectEventTypeConstants(): MutableMap = mutableMapOf( "topEngineReady" to mapOf("registrationName" to "onEngineReady"), diff --git a/android/src/test/java/com/splatkit/reactnative/SourceFetcherTest.kt b/android/src/test/java/com/splatkit/reactnative/SourceFetcherTest.kt index 26ff661..5388116 100644 --- a/android/src/test/java/com/splatkit/reactnative/SourceFetcherTest.kt +++ b/android/src/test/java/com/splatkit/reactnative/SourceFetcherTest.kt @@ -161,4 +161,29 @@ class SourceFetcherTest { assertTrue(live.exists()) assertFalse(old.exists()) } + + @Test + fun `finished files over the cap go oldest first and part files are left alone`() { + val dir = File(folder.root, "splatkit").apply { mkdirs() } + val now = System.currentTimeMillis() + val oldest = File(dir, "a.spz").apply { writeBytes(ByteArray(300)); setLastModified(now - 3000) } + val middle = File(dir, "b.spz").apply { writeBytes(ByteArray(300)); setLastModified(now - 2000) } + val newest = File(dir, "c.spz").apply { writeBytes(ByteArray(300)); setLastModified(now - 1000) } + val part = File(dir, "d.spz1.part").apply { writeBytes(ByteArray(300)) } + SourceFetcher(folder.root, cacheCapBytes = 700) { null } + assertFalse(oldest.exists()) + assertTrue(middle.exists()) + assertTrue(newest.exists()) + assertTrue(part.exists()) + } + + @Test + fun `a cache hit counts as use so the file is evicted last`() { + val cached = fetcher().fetch(url("/world.spz"), { false }) { _, _ -> } + val lastWeek = System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000L + cached.setLastModified(lastWeek) + fetcher().fetch(url("/world.spz"), { false }) { _, _ -> } + assertTrue(cached.lastModified() > lastWeek) + assertEquals(1, requests.get()) + } } diff --git a/src/SplatView.tsx b/src/SplatView.tsx index bbc082d..6c02755 100644 --- a/src/SplatView.tsx +++ b/src/SplatView.tsx @@ -11,7 +11,15 @@ import { } from './quality'; import type { CameraPose } from './SplatViewNativeComponent'; -export type { SplatSource, CameraPose } from './SplatViewNativeComponent'; +export type { + SplatSource, + CameraPose, + EngineReadyEvent, + WorldReadyEvent, + FailureEvent, + LoadProgressEvent, + StatsEvent, +} from './SplatViewNativeComponent'; export type { QualityPreset, QualitySettings } from './quality'; export type SplatViewProps = Omit & diff --git a/src/SplatViewNativeComponent.ts b/src/SplatViewNativeComponent.ts index b118ee9..5d96f93 100644 --- a/src/SplatViewNativeComponent.ts +++ b/src/SplatViewNativeComponent.ts @@ -21,7 +21,7 @@ export type SplatSource = { uri: string; }; -type EngineReadyEvent = { +export type EngineReadyEvent = { /** False when the device could not start the renderer; the view stays blank. */ available: boolean; /** GPU name and graphics API version as the driver reports them, empty when unavailable. */ @@ -51,15 +51,15 @@ export type CameraPose = { pitch?: CodegenTypes.Double; }; -type WorldReadyEvent = { +export type WorldReadyEvent = { splatCount: CodegenTypes.Int32; }; -type FailureEvent = { +export type FailureEvent = { message: string; }; -type LoadProgressEvent = { +export type LoadProgressEvent = { /** `world` or `collider`. */ kind: string; bytes: CodegenTypes.Double; @@ -67,7 +67,7 @@ type LoadProgressEvent = { total: CodegenTypes.Double; }; -type StatsEvent = { +export type StatsEvent = { fps: CodegenTypes.Double; frameMs: CodegenTypes.Double; gpuMs: CodegenTypes.Double; diff --git a/src/index.tsx b/src/index.tsx index 754cb5b..c29ac83 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,4 +6,9 @@ export type { QualitySettings, QualityPreset, CameraPose, + EngineReadyEvent, + WorldReadyEvent, + FailureEvent, + LoadProgressEvent, + StatsEvent, } from './SplatView'; From 2310a121bc6b673c1f3f2c8559755329a215d4ca Mon Sep 17 00:00:00 2001 From: Xget7 Date: Wed, 9 Sep 2026 23:37:41 -0300 Subject: [PATCH 14/14] README: measured numbers for the binding against the engine's own app, and the cleartext rule for remote worlds --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a73c583..7fe79f3 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,11 @@ Raise the floor with `expo-build-properties`: ["expo-build-properties", { "android": { "minSdkVersion": 29 } }] ``` +### Remote sources + +A release build refuses plain `http://` from Android 9 on, and the refusal arrives as `onWorldFailed` with "Cleartext HTTP traffic ... not permitted". +Serve worlds over `https://`, or allow cleartext in the host app's manifest or network security config for development. + ### Retrying, unloading, caching React Native resends a prop only when it changes, so after `onWorldFailed` a retry needs a new `uri` (a query string will do) or a new `key` on the view. @@ -123,15 +128,17 @@ Put a HUD or a joystick in a sibling view, as the example does. ## Performance -Measured on a Xiaomi Mi 9 (Adreno 640), the 500k splat World Labs kitchen, preset `medium`, same session, phone cooled between runs. -The engine's own benchmark reports the numbers; the binding adds nothing to the frame. +Measured on a Xiaomi Mi 9 (Adreno 640), the 500k splat World Labs kitchen with its collider, preset `medium`, release builds, camera at the origin, same session, phone cooled between runs. +The engine's own benchmark reports the numbers (one turn over 10 s); the binding adds nothing to the frame. -| Host | GPU ms p50 | frame ms | fps | +| Host | GPU ms p50 | frame ms p50 | fps | |---|---|---|---| -| Engine dev app | TBM | TBM | TBM | -| This package, example app | TBM | TBM | TBM | +| Engine dev app | 12.8 | 16.7 | 59.6 | +| This package, example app | 12.8 | 16.7 | 59.6 | -A remote world is streamed to disk and mapped, so loading a TBM MB file kept the Java heap under TBM MB. +`statsInterval={16}`, one event per frame, measured the same frame time as `0`, so a HUD can run at any rate. +A remote world is streamed to disk and mapped, so loading a 51 MB file (3.6 M splats) kept the Java heap under 8 MB; read into memory it peaked at 58 MB. +Two views on one screen both render; expect the frame rate to split between them. The engine's numbers per preset and per scene are in [docs/BENCHMARKS.md](https://github.com/Xget7/splatkit-android/blob/main/docs/BENCHMARKS.md). ## iOS