@g9tm/lynxcontrols-native
v1.4.7
Published
Android and iOS platform layer: the G9SqliteModule and G9HostModule native modules (Autolink) and their JS adapters.
Readme
@g9tm/lynxcontrols-native
Android and iOS platform layer for G9: three Lynx native modules and one element (<g9-video>), linked into a host by
Lynx Autolink from node_modules, plus their JS adapters.
| Module | JS adapter | Android (Kotlin) | iOS (Objective-C) |
| --- | --- | --- | --- |
| G9SqliteModule | createNativeSqliteDriver() | androidx.sqlite BundledSQLiteDriver 2.7.1 | system sqlite3 (iOS 15+, serial queue per session, sqlite3_interrupt) |
| G9HostModule | createNativeHostServices() | SharedPreferences, moveTaskToBack, InputMethodManager, haptics, insets, appDataDirectory() = filesDir | NSUserDefaults suite, resignFirstResponder, UIImpactFeedbackGenerator, safe-area insets, appDataDirectory() = Library |
| G9SpeechModule | createNativeSpeechToText() → services.speech (dictation into fields) | SpeechRecognizer (main thread, partial results), the microphone asked through the host activity's ActivityResultRegistry | SFSpeechRecognizer over AVAudioEngine; register it on the host's LynxConfig; Info.plist NSMicrophoneUsageDescription + NSSpeechRecognitionUsageDescription |
| <g9-video> element | createNativeMediaProvider() (@g9tm/lynxcontrols-native/media) | G9VideoUI: Media3 ExoPlayer 1.8.0 into a TextureView (opacity fades work); register G9VideoBehaviors.create() | G9VideoUI: AVQueuePlayer + AVPlayerLooper in an AVPlayerLayer; self-registers (LYNX_REGISTER_UI) |
import { createG9Sqlite, singleFileDatabaseLocator } from '@g9tm/lynxcontrols-sqlite';
import { createNativeHostServices, createNativeSqliteDriver } from '@g9tm/lynxcontrols-native';
const { host, preferences, appDataDirectory } = createNativeHostServices();
const root = await appDataDirectory(); // MAUI FileSystem.AppDataDirectory: the MAUI app's database stays found
const db = createG9Sqlite({ driver: createNativeSqliteDriver(), locator: singleFileDatabaseLocator(`${root}/g9.db`), /* … */ });
// <G9Provider services={{ clock, host, preferences }}>How it works
Dictation (
services.speech, MAUIIG9SpeechToText).createNativeSpeechToText()returnsnullwhereG9SpeechModuleis not linked, so the fields hide their microphone. One session at a time natively; the adapter numbers sessions, routes theg9:speechglobal event ({"session","text"}, interim hypotheses) to the session that asked, turns an abort intocancel(session)and settles everylistenonce (recognized/no-match/cancelled/denied; a recogniser fault rejects with aG9Error). Android: the manifest mergesRECORD_AUDIOand theRecognitionServicequery Android 11+ needs to see a recogniser; androidx.activity is compile-only (every host has it; a host activity that is not anActivityResultRegistryOwnergets the current permission state, never a prompt).isAvailable(locale)is "a recogniser exists" on Android (asking a locale needs a broadcast some recognisers never answer; an unsupported language ends asno-match) andSFSpeechRecognizer(locale).isAvailableon iOS.Video (the intro carousel's
services.media).<g9-video>props:src,paused,loop,muted,start-seconds,near-end-seconds,object-fit; eventsready,nearend,looped,error. The provider's player renders it with reactive props (no UI-method calls) and unmounts it ondispose(), which releases the native player.resolve()maps bundled assets withG9HostModule.assetUri(not-foundwhen the app does not ship the file). The near-end/loop and start-seek rules are the platform-free coresvideo/core/G9VideoCore.ktandios/src/core/G9VideoCore.m, tested on the JVM and on Linux.Contract. The driver passes the shared contract
@g9tm/lynxcontrols-sqlite/contract/cases.json(27 cases): on the JVM through the Kotlin core (npm run test:android-jvm), and in JS through the module protocol (src/native.test.ts).Threads. One single-thread executor per open database with a bounded queue (256; overflow →
busy). Replies are envelopes{ ok, value | error }, soG9SqliteErrorfields (code,sqliteCode,sql,completedStatements) survive the bridge.Cancellation. A batch aborted while queued in JS never reaches native code.
- Aborting a running batch calls
interrupt(session). - iOS:
sqlite3_interruptstops the statement, and the batch fails withinterrupted. - Android: androidx connections have no interrupt, so the statement finishes (
capabilities().interrupt === false).
- Aborting a running batch calls
Transport (
plan/specs/06§6).columnar(default): cell tags, float64 numbers and blob bytes as ArrayBuffers, text as strings.rows: tagged cells. Choose withcreateNativeSqliteDriver({ transport }).contract/transport-golden.jsonpins the bytes: the Kotlin encoder writes it, and the JS decoder test reads it.Values. Safe integers cross as numbers, others as
{ int64 }, BLOBs as ArrayBuffer. NUL in text,NaNandundefinedare rejected withmisuse, as on every driver.
Layout
types/platform-native-module.d.ts codegen input (`npm run codegen` → generated/, android/…/generated, ios/src/generated;
then scripts/android-bridge-types.mjs --fix gives the Android specs the parameter
types Lynx's bridge accepts — never run the bare generator)
scripts/android-bridge-types.mjs `--fix` after codegen; without it, the check (`npm run check:android-types`)
android/src/main/java/com/g9lynx/g9native/
sqlite/core/ pure Kotlin: G9SqliteCore, G9SqliteService (executors), G9SqliteTransport, values/errors
sqlite/androidx/ AndroidxSqlEngine (BundledSQLiteDriver)
host/core/ G9PreferencesCore
G9SqliteModule.kt, G9HostModule.kt, G9SpeechModule.kt, G9Bridge.kt Lynx modules (kapt + lynx-processor →
LynxLibraryProviderImpl)
android/src/test/ JVM tests: contract runner, core, service, transport golden, preferences
ios/src/core/ portable Objective-C (Foundation + sqlite3): G9SqliteCore, G9SqliteTransport, G9PreferencesCore
ios/src/ G9SqliteModule.m, G9HostModule.m, G9SpeechModule.m (dispatch queues, UIKit, Speech); register them
on the host's LynxConfig
ios/Tests/ G9NativeTestSuite (XCTest on a simulator; Linux harness in Tests/linux)
jvm/ JVM-only Gradle build of the core + tests (no Android SDK needed)
android-check/ cloud compile check of module + reference host against the published Lynx 4.1.0 classes (Media3,
androidx.sqlite and androidx.activity as compile-only stubs under stubs/), and
LynxBridgeSignatureTest: every @LynxMethod through Lynx's own LynxMethodWrapperThe Android package is com.g9lynx.g9native, not com.g9lynx.native: native is a reserved word in Java, so the
generated Java specs could not compile under that name.
The native identifiers are internal and did not follow the npm rename to @g9tm/lynxcontrols-native (decision D12):
the Android package com.g9lynx.g9native, the iOS pod g9lynx-native, the module classes G9SqliteModule and
G9HostModule, and the preference store g9lynx.preferences keep their names, so hosts and stored data are not
affected. The pod's version follows the package version (= the pinned G9MAUIControls version).
Commands
| Command | What it proves |
| --- | --- |
| npm run test:android-jvm | Kotlin core: contract 27/27 over both transports (JDBC engine), unit tests, JaCoCo gate (lines ≥ 95 %, branches ≥ 85 %) |
| gradle -p packages/native/jvm check -Pg9.bundled=true | The same on androidx BundledSQLiteDriver (needs Google Maven) |
| npm run check:android-module | Module and host compile against Lynx 4.1.0, and the Autolink provider is generated |
| npm run test:ios-core | iOS core on Linux (clang + GNUstep): contract 27/27 × 2, golden byte-identical with Kotlin, interrupt, ARC syntax |
| npm run bench:transport | JS decode cost of columnar vs rows |
Host integration: docs/integration/android.md and
docs/integration/ios.md (reference hosts: apps/android-host, apps/ios-host).
