diff --git a/src/HingeObserver.ts b/src/HingeObserver.ts index d06ab08..a3f4587 100644 --- a/src/HingeObserver.ts +++ b/src/HingeObserver.ts @@ -36,7 +36,8 @@ export function mapHinges(hinges: readonly NativeHinge[]): readonly Hinge[] { /** * Creates an observer for an existing React root tag (available from RootTagContext). - * Creation reads the native cache once; subscribe to keep the snapshot current. + * Creation reads the native cache once as a seed; further updates arrive with the + * onHingesChange event, and subscribing triggers a native replay of the current snapshot. * Multiple observers for the same root share native observation. No provider is needed. */ export function createHingeObserver(root: number | RootTag): HingeObserver { @@ -56,11 +57,6 @@ export function createHingeObserver(root: number | RootTag): HingeObserver { return true; }; update(NativeHinges.getSnapshot(rootTag).hinges); - const refresh = () => { - if (update(NativeHinges.getSnapshot(rootTag).hinges)) { - for (const callback of listeners) callback(); - } - }; return { get: () => snapshot, subscribe: (listener) => { @@ -69,10 +65,11 @@ export function createHingeObserver(root: number | RootTag): HingeObserver { if (listeners.size === 1) { subscription = NativeHinges.onHingesChange((event) => { if (event.rootTag !== rootTag) return; - refresh(); + if (update(event.hinges)) { + for (const callback of listeners) callback(); + } }); NativeHinges.startObserving(rootTag); - refresh(); } return () => { if (!listeners.delete(notify)) return; diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index 7ad29a7..0de8ee4 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -18,7 +18,13 @@ jest.mock('../HingesModule', () => ({ __esModule: true, default: { getSnapshot: jest.fn((root: number) => ({ hinges: mockSnapshots.get(root) ?? [] })), - startObserving: jest.fn(), + // Native replays the current snapshot asynchronously after startObserving. + startObserving: jest.fn((rootTag: number) => { + queueMicrotask(() => { + const hinges = mockSnapshots.get(rootTag) ?? []; + for (const listener of mockListeners) listener({ rootTag, hinges }); + }); + }), stopObserving: jest.fn(), onHingesChange: jest.fn((listener: (event: HingesChangeEvent) => void) => { mockListeners.add(listener); @@ -72,13 +78,13 @@ it('isolates roots, shares one observation across subscriptions, and releases on expect(mockListeners.size).toBe(0); }); -it('does not replace a newer cached snapshot with a queued older event', () => { +it('applies event payloads directly without re-reading the native snapshot', () => { const observer = createHingeObserver(1); const off = observer.subscribe(jest.fn()); + expect(NativeHinges.getSnapshot).toHaveBeenCalledTimes(1); emit(1, native); - const snapshot = observer.get(); - for (const listener of mockListeners) listener({ rootTag: 1, hinges: [] }); - expect(observer.get()).toBe(snapshot); + expect(observer.get()).toEqual([{ status: 'partiallyOpen', angle: Math.PI / 2 }]); + expect(NativeHinges.getSnapshot).toHaveBeenCalledTimes(1); off(); }); @@ -149,12 +155,16 @@ it('throws a clear error when useHinges renders without a RootTagContext provide } }); -it('refreshes state that changed between observer creation and subscription', () => { +it('delivers state that changed between observer creation and subscription via the startObserving replay', async () => { const observer = createHingeObserver(1); expect(observer.get()).toEqual([]); mockSnapshots.set(1, native); const listener = jest.fn(); const off = observer.subscribe(listener); + expect(observer.get()).toEqual([]); + expect(listener).not.toHaveBeenCalled(); + await Promise.resolve(); + expect(NativeHinges.getSnapshot).toHaveBeenCalledTimes(1); expect(observer.get()).toEqual([{ status: 'partiallyOpen', angle: Math.PI / 2 }]); expect(listener).toHaveBeenCalledTimes(1); off(); diff --git a/website/docs/api.md b/website/docs/api.md index b27705f..017b47f 100644 --- a/website/docs/api.md +++ b/website/docs/api.md @@ -58,7 +58,7 @@ Subscribe callbacks receive no arguments. Read `get()` to obtain the latest snap function createHingeObserver(rootTag: number | RootTag): HingeObserver; ``` -Creates an observer for an existing React root. Obtain its tag from React Native's `RootTagContext` or a native host integration. Creation reads the native cache once; `get()` returns the latest observed snapshot and subscriptions keep it current. The last native subscriber releases observation and the cache. See [observer usage](./observers.md). +Creates an observer for an existing React root. Obtain its tag from React Native's `RootTagContext` or a native host integration. Creation reads the native cache once as a seed; `get()` returns the latest observed snapshot, and subscribing triggers a native replay of the current snapshot, with later updates arriving as native change events. The last native subscriber releases observation and the cache. See [observer usage](./observers.md). ## AnimatedHingesProvider diff --git a/website/docs/observers.md b/website/docs/observers.md index ace0b46..47ef9b4 100644 --- a/website/docs/observers.md +++ b/website/docs/observers.md @@ -19,6 +19,6 @@ unsubscribe(); Creation seeds the snapshot from the native cache; `get()` synchronously reads the JS-owned snapshot. Returned arrays and entries are immutable, and equivalent snapshots retain their identity. Before a native reading is available it returns `[]`. -`subscribe()` starts observation for the selected root. Callbacks receive no arguments; read `get()` for the current value. Multiple subscriptions on this observer share one native subscription; separate observers and animated hooks are reference-counted natively. Unsubscribing the final consumer releases native observation and clears its native cache. The observer retains its last delivered snapshot until a later subscription refreshes it. +`subscribe()` starts observation for the selected root, which triggers a native replay of the current snapshot as an event; later updates also arrive with the native change event. Callbacks receive no arguments; read `get()` for the current value. Multiple subscriptions on this observer share one native subscription; separate observers are reference-counted natively. Unsubscribing the final consumer releases native observation and clears its native cache. The observer keeps its last delivered snapshot after unsubscribing. Creating an observer or calling `get()` alone does not start native observation. Subscribe when readings must remain current. No process-wide root is selected implicitly.