export const DEVICE_ADAPTER_CONTRACT_VERSION = "nodedc.device-adapter.v1"; const ADAPTER_REF_RE = /^[a-z][a-z0-9-]{1,62}$/; const PROFILE_REF_RE = /^[a-z][a-z0-9._-]{2,127}$/; const RUNTIME_PACKAGE_REF_RE = /^@[a-z0-9-]+\/[a-z0-9-]+$/; export function defineDeviceAdapter(input) { assertPlainObject(input, "device_adapter"); if (input.contractVersion !== DEVICE_ADAPTER_CONTRACT_VERSION) { throw new TypeError("device_adapter_contract_version_invalid"); } const adapterRef = normalizeRef( input.adapterRef, ADAPTER_REF_RE, "device_adapter_ref_invalid", ); const runtimePackageRef = normalizeRef( input.runtimePackageRef, RUNTIME_PACKAGE_REF_RE, "device_adapter_runtime_package_ref_invalid", ); if (!Array.isArray(input.profiles) || input.profiles.length === 0) { throw new TypeError("device_adapter_profiles_required"); } const profiles = input.profiles.map((profile) => normalizeProfile(profile, adapterRef) ); if (new Set(profiles.map((profile) => profile.profileRef)).size !== profiles.length) { throw new TypeError("device_adapter_profile_duplicate"); } if (typeof input.createSession !== "function") { throw new TypeError("device_adapter_session_factory_required"); } return deepFreeze({ contractVersion: DEVICE_ADAPTER_CONTRACT_VERSION, adapterRef, runtimePackageRef, profiles, createSession: input.createSession, }); } export function createDeviceAdapterRegistry({ adapters = [] } = {}) { if (!Array.isArray(adapters)) { throw new TypeError("device_adapter_registry_adapters_invalid"); } const byAdapterRef = new Map(); const byProfileRef = new Map(); for (const adapter of adapters) { const normalized = defineDeviceAdapter(adapter); if (byAdapterRef.has(normalized.adapterRef)) { throw new TypeError("device_adapter_registry_adapter_duplicate"); } byAdapterRef.set(normalized.adapterRef, normalized); for (const profile of normalized.profiles) { if (byProfileRef.has(profile.profileRef)) { throw new TypeError("device_adapter_registry_profile_duplicate"); } byProfileRef.set(profile.profileRef, Object.freeze({ adapter: normalized, profile, })); } } return Object.freeze({ adapterRefs: Object.freeze([...byAdapterRef.keys()].sort()), profileRefs: Object.freeze([...byProfileRef.keys()].sort()), getAdapter(adapterRef) { const normalized = normalizeRef( adapterRef, ADAPTER_REF_RE, "device_adapter_ref_invalid", ); const adapter = byAdapterRef.get(normalized); if (!adapter) throw new TypeError("device_adapter_not_allowlisted"); return adapter; }, resolveProfile(profileRef) { const normalized = normalizeRef( profileRef, PROFILE_REF_RE, "device_adapter_profile_ref_invalid", ); const registration = byProfileRef.get(normalized); if (!registration) { throw new TypeError("device_adapter_profile_not_allowlisted"); } return registration; }, }); } export function assertDeviceAdapterSession(session) { assertPlainObject(session, "device_adapter_session"); for (const method of [ "parseHeader", "buildHeaderAcknowledgement", "parseMessage", "buildMessageAcknowledgement", ]) { if (typeof session[method] !== "function") { throw new TypeError(`device_adapter_session_method_missing:${method}`); } } return session; } function normalizeProfile(profile, adapterRef) { assertPlainObject(profile, "device_adapter_profile"); const profileRef = normalizeRef( profile.profileRef, PROFILE_REF_RE, "device_adapter_profile_ref_invalid", ); if (profile.adapterRef != null && profile.adapterRef !== adapterRef) { throw new TypeError("device_adapter_profile_adapter_mismatch"); } const maxBufferedBytes = Number(profile?.framing?.maxBufferedBytes); if ( !Number.isSafeInteger(maxBufferedBytes) || maxBufferedBytes < 1024 || maxBufferedBytes > 256 * 1024 ) { throw new TypeError("device_adapter_profile_buffer_limit_invalid"); } return deepFreeze({ ...profile, profileRef, adapterRef }); } function normalizeRef(value, pattern, errorCode) { if (typeof value !== "string" || !pattern.test(value)) { throw new TypeError(errorCode); } return value; } function assertPlainObject(value, label) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new TypeError(`${label}_invalid`); } } function deepFreeze(value) { if (!value || typeof value !== "object" || Object.isFrozen(value)) { return value; } Object.values(value).forEach(deepFreeze); return Object.freeze(value); }