one
Version:
One is a React framework for web, iOS, and Android apps.
108 lines (100 loc) • 4.15 kB
text/typescript
import { getNativeSyncFactory, type NativeSyncHost } from './syncNative'
// observable state shared between JavaScript and native views: one handle feeds
// any number of controlled props, so every bound view converges on the same value.
//
// the value lives in the native registry, owned outside every JS runtime. the
// JSI host object reads and writes it synchronously from the JS runtime and
// the UI worklet runtime alike; SwiftUI and Compose models observe the same
// entry directly, so bound views converge without a React render. React is
// never the source of truth: components only subscribe (see useNativeState).
export const SYNC_STATE_ID_KEY = '__one_sync_state_id__' as const
export const SYNC_STATE_BRAND = '__one_sync_state__' as const
export type SyncStateListener<T> = (value: T) => void
export type SyncState<T> = {
// the current value, read synchronously from the native entry.
value: T
// React Compiler compliant read/write alternatives to `.value`.
get(): T
set(value: T): void
// single listener invoked through the native entry on every write, before
// subscribers. assign null to clear. the initial value does not fire
// onChange, and setting the value it already holds is a no-op.
onChange: SyncStateListener<T> | null
// subscribe a JS listener; returns an unsubscribe function.
subscribe(listener: SyncStateListener<T>): () => void
// destroys the native entry. manual: see NativeSyncHost.release.
release(): void
// stable snapshot for useSyncExternalStore: same reference until set().
getSnapshot(): T
readonly [SYNC_STATE_BRAND]: true
readonly [SYNC_STATE_ID_KEY]: number
}
export function isSyncState(value: unknown): value is SyncState<unknown> {
if (value == null || typeof value !== 'object') return false
const obj = value as Partial<SyncState<unknown>>
return (
obj[SYNC_STATE_BRAND] === true &&
typeof obj[SYNC_STATE_ID_KEY] === 'number' &&
obj[SYNC_STATE_ID_KEY] !== 0
)
}
// extracts the numeric id for generated props that bind by handle.
export function getSyncStateId(state: object | null | undefined): number | undefined {
if (!state) return undefined
return (state as { [SYNC_STATE_ID_KEY]?: number })[SYNC_STATE_ID_KEY]
}
export function createSyncState<T>(initial: T): SyncState<T> {
const host: NativeSyncHost<T> = getNativeSyncFactory().create<T>(initial)
let onChange: SyncStateListener<T> | null = null
const listeners = new Set<SyncStateListener<T>>()
// the last value JS subscribers were told about. a native view writes the
// entry itself and then reports the edit through set(), so the entry alone
// cannot tell whether subscribers have seen a value.
let published = host.get()
const state = {
get value(): T {
return host.get()
},
set value(next: T) {
state.set(next)
},
get(): T {
return host.get()
},
set(next: T): void {
// the native entry invokes onChange synchronously inside set; the JS
// subscribers follow, so onChange always lands first. a native edit has
// already written the entry, so only its subscribers remain to notify.
if (!Object.is(host.get(), next)) host.set(next)
// React-style bailout: a value subscribers already have notifies nothing.
if (Object.is(published, next)) return
published = next
// copy: listeners may subscribe/unsubscribe (or set) reentrantly.
for (const listener of [...listeners]) listener(next)
},
get onChange(): SyncStateListener<T> | null {
return onChange
},
set onChange(next: SyncStateListener<T> | null | undefined) {
onChange = next ?? null
host.setOnChange(onChange)
},
subscribe(listener: SyncStateListener<T>): () => void {
listeners.add(listener)
return () => {
listeners.delete(listener)
}
},
release(): void {
listeners.clear()
onChange = null
host.release()
},
getSnapshot(): T {
return host.get()
},
[SYNC_STATE_BRAND]: true as const,
[SYNC_STATE_ID_KEY]: host.id as number,
} satisfies SyncState<T>
return state
}