feat: complete STEP-078 V4 bridge
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-13 16:10:49 +08:00
parent 4b5192e224
commit 73779b89bc
18 changed files with 403 additions and 2 deletions

View File

@@ -104,6 +104,30 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
catch (error: IllegalStateException) { reject(promise, "NATIVE_REJECTED", error.message ?: "WeatherRuntime rejected the field") }
}
}
/** V4 transports only a bounded local raw QBIN file plus small primitive control-plane values. */
@ReactMethod override fun submitEncodedField(reactTag: Double, fieldKey: String, fieldID: String, generation: Double, dataVersion: Double, contractJson: String, qbinFileUri: String, promise: Promise) {
if (!validEncodedId(fieldKey) || !validEncodedId(fieldID) || !validSafeInteger(generation, true) || !validSafeInteger(dataVersion, false) || contractJson.isEmpty() || contractJson.toByteArray(Charsets.UTF_8).size > MAX_CONTRACT_UTF8_BYTES) return reject(promise, "INVALID_ARGUMENT", "invalid V4 encoded field control-plane argument")
val payload = try { readLocalPayload(qbinFileUri) } catch (e: Exception) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", e.message ?: "QBIN file is unreadable") }
if (payload.isEmpty()) return reject(promise, "PAYLOAD_FILE_UNREADABLE", "QBIN file is empty")
withRuntime(reactTag, promise) { runtime -> try { promise.resolve(runtime.submitEncodedField(fieldKey, fieldID, generation.toLong(), dataVersion.toLong(), contractJson, payload).name) } catch (e: IllegalStateException) { reject(promise, if (e.message == "SOURCE_MISSING") "SOURCE_MISSING" else "RUNTIME_DISPOSED", e.message ?: "V4 runtime unavailable") } catch (e: IllegalArgumentException) { reject(promise, "INVALID_ARGUMENT", e.message ?: "invalid encoded field") } }
}
@ReactMethod override fun clearEncodedField(reactTag: Double, fieldKey: String, promise: Promise) { if (!validEncodedId(fieldKey)) return reject(promise,"INVALID_ARGUMENT","invalid fieldKey"); command(reactTag,promise) { it.clearEncodedField(fieldKey) } }
@ReactMethod override fun clearAllEncodedFields(reactTag: Double, promise: Promise) = command(reactTag,promise) { it.clearAllEncodedFields() }
@ReactMethod override fun setEncodedForecastTime(reactTag: Double, timestampMs: Double, promise: Promise) { if(!validSafeInteger(timestampMs,false)) return reject(promise,"INVALID_ARGUMENT","encoded forecast time must be safe integer"); command(reactTag,promise) { it.setEncodedForecastTime(timestampMs.toLong()) } }
@ReactMethod override fun setEncodedForecastPlaybackState(reactTag: Double, playback: String, promise: Promise) { if(playback != "PLAY" && playback != "PAUSE") return reject(promise,"INVALID_ARGUMENT","invalid encoded playback"); command(reactTag,promise) { it.setEncodedForecastPlaybackState(playback == "PLAY") } }
@ReactMethod override fun setEncodedForecastPlaybackSpeed(reactTag: Double, speed: Double, promise: Promise) { if(!speed.isFinite() || speed <= 0 || speed > Float.MAX_VALUE) return reject(promise,"INVALID_ARGUMENT","invalid encoded playback speed"); command(reactTag,promise) { it.setEncodedForecastPlaybackSpeed(speed.toFloat()) } }
@ReactMethod override fun setEncodedColorBinding(reactTag: Double, fieldKey: String, semanticID: Double, groupID: Double, expectedValueClass: String, expectedUnitID: Double, promise: Promise) {
if (!validEncodedId(fieldKey) || !validUInt16(semanticID, true) || !validUInt16(groupID, false) || !validUInt16(expectedUnitID, false)) return reject(promise,"INVALID_ARGUMENT","invalid encoded color binding selector")
val valueClass = when (expectedValueClass) { "SCALAR" -> 0; "DIRECTION_ANGLE" -> 2; else -> return reject(promise,"INVALID_ARGUMENT","invalid encoded color binding valueClass") }
command(reactTag,promise) { it.setEncodedColorBinding(fieldKey, semanticID.toInt(), groupID.toInt(), valueClass, expectedUnitID.toInt()) }
}
@ReactMethod override fun setEncodedParticleBinding(reactTag: Double, fieldKey: String, mode: String, uSemanticID: Double, uGroupID: Double, vSemanticID: Double, vGroupID: Double, directionSemanticID: Double, directionGroupID: Double, expectedUnitID: Double, promise: Promise) {
if (!validEncodedId(fieldKey) || !validUInt16(expectedUnitID, false)) return reject(promise,"INVALID_ARGUMENT","invalid encoded particle binding")
val vector = mode == "VECTOR_COMPONENTS" && validUInt16(uSemanticID, true) && validUInt16(uGroupID, false) && validUInt16(vSemanticID, true) && validUInt16(vGroupID, false) && inactiveSelector(directionSemanticID, directionGroupID)
val direction = mode == "DIRECTION_ANGLE" && inactiveSelector(uSemanticID, uGroupID) && inactiveSelector(vSemanticID, vGroupID) && validUInt16(directionSemanticID, true) && validUInt16(directionGroupID, false)
if (!vector && !direction) return reject(promise,"INVALID_ARGUMENT","active encoded particle selectors must be uint16 with non-zero semanticID; inactive slots are exactly zero")
command(reactTag,promise) { it.setEncodedParticleBinding(fieldKey, if (vector) 0 else 1, uSemanticID.toInt(), uGroupID.toInt(), vSemanticID.toInt(), vGroupID.toInt(), directionSemanticID.toInt(), directionGroupID.toInt(), expectedUnitID.toInt()) }
}
@ReactMethod override fun setWeatherEnabled(reactTag: Double, enabled: Boolean, promise: Promise) = command(reactTag, promise) { it.setWeatherEnabled(enabled) }
@ReactMethod override fun setWeatherColorVisible(reactTag: Double, visible: Boolean, promise: Promise) = command(reactTag, promise) { it.setWeatherColorVisible(visible) }
@ReactMethod override fun setParticleVisible(reactTag: Double, visible: Boolean, promise: Promise) = command(reactTag, promise) { it.setParticleVisible(visible) }
@@ -162,7 +186,7 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
} catch (error: IllegalStateException) {
// WeatherRuntime rejects disabled/disposed commands by contract. Do not let an
// exception escape the UI queue or turn a stale command into an implicit success.
reject(promise, "NATIVE_REJECTED", error.message ?: "WeatherRuntime rejected the command")
reject(promise, if (error.message == "SOURCE_MISSING") "SOURCE_MISSING" else "NATIVE_REJECTED", error.message ?: "WeatherRuntime rejected the command")
}
}
private fun floatCommand(reactTag: Double, value: Double, promise: Promise, fn: (WeatherRuntime, Float) -> WeatherRuntime.CommandResult) {
@@ -270,6 +294,10 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
}
private fun presence(exists: Boolean) = if (exists) "present" else "missing"
private fun validEncodedId(value: String) = value.isNotEmpty() && value.length <= 64
private fun validUInt16(value: Double, positive: Boolean) = value.isFinite() && value == kotlin.math.floor(value) && value >= if (positive) 1 else 0 && value <= 0xffff
private fun inactiveSelector(semanticID: Double, groupID: Double) = semanticID == 0.0 && groupID == 0.0
private fun validSafeInteger(value: Double, positive: Boolean) = value.isFinite() && value <= MAX_SAFE_INTEGER && value >= if (positive) 1 else 0 && value == kotlin.math.floor(value)
private fun reject(promise: Promise, code: String, message: String) = promise.reject(code, message)
private object WeatherIds {
@@ -280,6 +308,8 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
companion object {
const val NAME = "WeatherRuntimeBridge"
private const val MAX_METADATA_UTF8_BYTES = 1_048_576
private const val MAX_CONTRACT_UTF8_BYTES = 256 * 1024
private const val MAX_SAFE_INTEGER = 9_007_199_254_740_991.0
private const val MAX_PAYLOAD_BYTES = 64L * 1024L * 1024L
private const val DEFAULT_READ_BUFFER_BYTES = 16 * 1024
}

View File

@@ -1,5 +1,12 @@
import { getNativeFieldRuntimeBridge } from "./NativeWeatherRuntimeBridge";
import type { ColorChannel, CommandResult, FieldColorStop, FieldPayloadRefs, FieldSubmitResult, FieldValueSource, PlaybackState, RuntimeSnapshot, TargetFps } from "./types";
import type { ColorChannel, CommandResult, EncodedColorBindingInput, EncodedFieldRuntimeMetadata, EncodedFieldSubmitResult, EncodedParticleBindingInput, FieldColorStop, FieldPayloadRefs, FieldSubmitResult, FieldValueSource, PlaybackState, ResolvedFieldProfileContractInput, RuntimeSnapshot, TargetFps } from "./types";
const MAX_SAFE = Number.MAX_SAFE_INTEGER;
const MAX_ID_LENGTH = 64;
const MAX_CONTRACT_BYTES = 256 * 1024;
const encodedString = (value: string, name: string) => { if (typeof value !== "string" || value.length === 0 || value.length > MAX_ID_LENGTH) throw new Error(`${name} must be a non-empty string of at most ${MAX_ID_LENGTH} characters`); };
const uint = (value: number, name: string, maximum = MAX_SAFE, positive = false) => { if (!Number.isSafeInteger(value) || value < (positive ? 1 : 0) || value > maximum) throw new Error(`${name} must be a ${positive ? "positive" : "non-negative"} safe integer`); };
function encodedContractJson(contract: ResolvedFieldProfileContractInput): string { encodedString(contract.profileID, "profileID"); uint(contract.contractSchemaVersion, "contractSchemaVersion", 0xffff, true); if (contract.contractSchemaVersion !== 1) throw new Error("unsupported contractSchemaVersion"); uint(contract.profileVersion, "profileVersion", 0xffffffff, true); if (!Array.isArray(contract.requiredPlanes) || contract.requiredPlanes.length === 0 || contract.requiredPlanes.length > 64) throw new Error("requiredPlanes must contain 1..64 entries"); for (const p of contract.requiredPlanes) { uint(p.semanticID, "semanticID", 0xffff, true); uint(p.unitID, "unitID", 0xffff); uint(p.groupID, "groupID", 0xffff); if (!p.requiredFlag || !["SCALAR", "VELOCITY_COMPONENT", "DIRECTION_ANGLE", "VALIDITY"].includes(p.valueClass) || !["NONE", "U_EAST", "V_NORTH", "W_UP"].includes(p.component)) throw new Error("invalid required plane contract"); } const json = JSON.stringify(contract); if (new TextEncoder().encode(json).length > MAX_CONTRACT_BYTES) throw new Error("contractJson exceeds 256 KiB"); return json; }
function nativeBridge() {
const bridge = getNativeFieldRuntimeBridge();
@@ -21,6 +28,14 @@ export const FieldRuntime = {
payloadRefs.scalarFileUri ?? "",
payloadRefs.vectorFileUri ?? "",
),
submitEncodedField: (reactTag: number, fieldKey: string, metadata: EncodedFieldRuntimeMetadata, contract: ResolvedFieldProfileContractInput, qbinFileUri: string): Promise<EncodedFieldSubmitResult> => { encodedString(fieldKey, "fieldKey"); encodedString(metadata.fieldID, "fieldID"); uint(metadata.generation, "generation", MAX_SAFE, true); uint(metadata.dataVersion, "dataVersion"); if (typeof qbinFileUri !== "string" || !(qbinFileUri.startsWith("/") || qbinFileUri.startsWith("file://"))) return Promise.reject(new Error("qbinFileUri must be an absolute local path or file:// URI")); return nativeBridge().submitEncodedField(reactTag, fieldKey, metadata.fieldID, metadata.generation, metadata.dataVersion, encodedContractJson(contract), qbinFileUri); },
setEncodedColorBinding: (reactTag: number, binding: EncodedColorBindingInput): Promise<CommandResult> => { encodedString(binding.fieldKey, "fieldKey"); uint(binding.semanticID, "semanticID", 0xffff, true); uint(binding.groupID, "groupID", 0xffff); uint(binding.expectedUnitID, "expectedUnitID", 0xffff); if (!["SCALAR", "DIRECTION_ANGLE"].includes(binding.expectedValueClass)) return Promise.reject(new Error("invalid encoded color binding valueClass")); return nativeBridge().setEncodedColorBinding(reactTag, binding.fieldKey, binding.semanticID, binding.groupID, binding.expectedValueClass, binding.expectedUnitID); },
setEncodedParticleBinding: (reactTag: number, binding: EncodedParticleBindingInput): Promise<CommandResult> => { encodedString(binding.fieldKey, "fieldKey"); uint(binding.expectedUnitID, "expectedUnitID", 0xffff); const inactive = { semanticID: 0, groupID: 0 }; const valid = (plane: { semanticID: number; groupID: number }) => { uint(plane.semanticID, "selector semanticID", 0xffff, true); uint(plane.groupID, "selector groupID", 0xffff); }; let u = inactive, v = inactive, direction = inactive; if (binding.mode === "VECTOR_COMPONENTS" && binding.uPlane && binding.vPlane && !binding.directionPlane) { u = binding.uPlane; v = binding.vPlane; valid(u); valid(v); } else if (binding.mode === "DIRECTION_ANGLE" && binding.directionPlane && !binding.uPlane && !binding.vPlane) { direction = binding.directionPlane; valid(direction); } else return Promise.reject(new Error("encoded particle binding requires exactly U/V or direction selector; inactive ABI slots are zero")); return nativeBridge().setEncodedParticleBinding(reactTag, binding.fieldKey, binding.mode, u.semanticID, u.groupID, v.semanticID, v.groupID, direction.semanticID, direction.groupID, binding.expectedUnitID); },
setEncodedForecastTime: (reactTag: number, timestampMs: number): Promise<CommandResult> => { uint(timestampMs, "forecast timestamp"); return nativeBridge().setEncodedForecastTime(reactTag, timestampMs); },
setEncodedForecastPlaybackState: (reactTag: number, playback: "PLAY" | "PAUSE"): Promise<CommandResult> => nativeBridge().setEncodedForecastPlaybackState(reactTag, playback),
setEncodedForecastPlaybackSpeed: (reactTag: number, speed: number): Promise<CommandResult> => !Number.isFinite(speed) || speed <= 0 ? Promise.reject(new Error("encoded playback speed must be finite and > 0")) : nativeBridge().setEncodedForecastPlaybackSpeed(reactTag, speed),
clearEncodedField: (reactTag: number, fieldKey: string): Promise<CommandResult> => { encodedString(fieldKey, "fieldKey"); return nativeBridge().clearEncodedField(reactTag, fieldKey); },
clearAllEncodedFields: (reactTag: number): Promise<CommandResult> => nativeBridge().clearAllEncodedFields(reactTag),
/** Explicit v1 compatibility only; v2 submitField always receives FieldPayloadRefs. */
submitLegacyVectorField: (reactTag: number, metadataJson: string, payloadFileUri: string): Promise<FieldSubmitResult> => nativeBridge().submitWindField(reactTag, metadataJson, payloadFileUri),
setFieldValueSource: (reactTag: number, source: FieldValueSource): Promise<CommandResult> => nativeBridge().setFieldValueSource(reactTag, source),

View File

@@ -6,6 +6,7 @@ type PlaybackState = "PLAY" | "PAUSE";
type TargetFps = "AUTO" | "FPS_30" | "FPS_60";
type CommandResult = "ACCEPTED" | "REJECTED";
type FieldSubmitResult = "REJECTED_SOURCE_MODE" | "REJECTED_ADAPTER" | "REJECTED_INCOMPATIBLE" | "REJECTED_NON_MONOTONIC" | "REJECTED_CAPACITY" | "ACCEPTED_PENDING_INITIAL_WINDOW" | "INITIAL_WINDOW_READY" | "ACCEPTED_NEXT_SNAPSHOT" | "DUPLICATE" | "UNKNOWN";
type EncodedFieldSubmitResult = "ACCEPTED_INSERTED" | "ACCEPTED_REPLACED_REVISION" | "DUPLICATE" | "REJECTED_DECODE" | "REJECTED_CONTRACT" | "REJECTED_INVALID_FIELD_KEY" | "REJECTED_IDENTITY_CONFLICT" | "REJECTED_STALE_REVISION" | "REJECTED_INCOMPATIBLE" | "REJECTED_CAPACITY";
type ColorStop = { speedMps: number; red: number; green: number; blue: number; alpha: number };
type RuntimeSnapshot = { state: RuntimeState; playbackState: PlaybackState; targetFps: TargetFps; disposed: boolean };
export interface Spec extends TurboModule {
@@ -18,6 +19,15 @@ export interface Spec extends TurboModule {
// Keep the codegen ABI to primitive parameters. Empty scalar/vector strings are explicit
// optional-plane markers at this boundary only; the public API keeps FieldPayloadRefs.
submitField(reactTag: number, metadataJson: string, validityFileUri: string, scalarFileUri: string, vectorFileUri: string): Promise<FieldSubmitResult>;
submitEncodedField(reactTag: number, fieldKey: string, fieldID: string, generation: number, dataVersion: number, contractJson: string, qbinFileUri: string): Promise<EncodedFieldSubmitResult>;
setEncodedColorBinding(reactTag: number, fieldKey: string, semanticID: number, groupID: number, expectedValueClass: string, expectedUnitID: number): Promise<CommandResult>;
// All selector slots are primitive for Codegen. Exactly (0,0) marks an inactive selector.
setEncodedParticleBinding(reactTag: number, fieldKey: string, mode: string, uSemanticID: number, uGroupID: number, vSemanticID: number, vGroupID: number, directionSemanticID: number, directionGroupID: number, expectedUnitID: number): Promise<CommandResult>;
setEncodedForecastTime(reactTag: number, timestampMs: number): Promise<CommandResult>;
setEncodedForecastPlaybackState(reactTag: number, playback: string): Promise<CommandResult>;
setEncodedForecastPlaybackSpeed(reactTag: number, speed: number): Promise<CommandResult>;
clearEncodedField(reactTag: number, fieldKey: string): Promise<CommandResult>;
clearAllEncodedFields(reactTag: number): Promise<CommandResult>;
setFieldValueSource(reactTag: number, source: "SCALAR" | "VECTOR_MAGNITUDE"): Promise<CommandResult>;
setParticleAdvectionScale(reactTag: number, scale: number): Promise<CommandResult>;
setWeatherEnabled(reactTag: number, enabled: boolean): Promise<CommandResult>;

View File

@@ -3,6 +3,14 @@ export type PlaybackState = "PLAY" | "PAUSE";
export type TargetFps = "AUTO" | "FPS_30" | "FPS_60";
export type CommandResult = "ACCEPTED" | "REJECTED";
export type FieldSubmitResult = "REJECTED_SOURCE_MODE" | "REJECTED_ADAPTER" | "REJECTED_INCOMPATIBLE" | "REJECTED_NON_MONOTONIC" | "REJECTED_CAPACITY" | "ACCEPTED_PENDING_INITIAL_WINDOW" | "INITIAL_WINDOW_READY" | "ACCEPTED_NEXT_SNAPSHOT" | "DUPLICATE" | "UNKNOWN";
/** Stable V4/Core outcome; unlike V3 rejection is a resolved result. */
export type EncodedFieldSubmitResult = "ACCEPTED_INSERTED" | "ACCEPTED_REPLACED_REVISION" | "DUPLICATE" | "REJECTED_DECODE" | "REJECTED_CONTRACT" | "REJECTED_INVALID_FIELD_KEY" | "REJECTED_IDENTITY_CONFLICT" | "REJECTED_STALE_REVISION" | "REJECTED_INCOMPATIBLE" | "REJECTED_CAPACITY";
export type EncodedFieldRuntimeMetadata = Readonly<{ fieldID: string; generation: number; dataVersion: number }>;
export type RequiredPlaneContractInput = Readonly<{ semanticID: number; valueClass: "SCALAR" | "VELOCITY_COMPONENT" | "DIRECTION_ANGLE" | "VALIDITY"; unitID: number; groupID: number; component: "NONE" | "U_EAST" | "V_NORTH" | "W_UP"; requiredFlag: boolean }>;
/** Frozen V4 JSON field names match ResolvedFieldProfileContract exactly. */
export type ResolvedFieldProfileContractInput = Readonly<{ contractSchemaVersion: number; profileID: string; profileVersion: number; allowAdditionalPlanes: boolean; sharedValidityRequired: boolean; timeRelation: "ANY" | "VALID_GTE_REFERENCE" | "VALID_EQ_REFERENCE"; requiredPlanes: readonly RequiredPlaneContractInput[] }>;
export type EncodedColorBindingInput = Readonly<{ fieldKey: string; semanticID: number; groupID: number; expectedValueClass: "SCALAR" | "DIRECTION_ANGLE"; expectedUnitID: number }>;
export type EncodedParticleBindingInput = Readonly<{ fieldKey: string; mode: "VECTOR_COMPONENTS" | "DIRECTION_ANGLE"; uPlane?: Readonly<{ semanticID: number; groupID: number }>; vPlane?: Readonly<{ semanticID: number; groupID: number }>; directionPlane?: Readonly<{ semanticID: number; groupID: number }>; expectedUnitID: number }>;
export type FieldPayloadRefs = Readonly<
{ validityFileUri: string } & (
| { scalarFileUri: string; vectorFileUri?: string }