Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions src/HingeObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) => {
Expand All @@ -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;
Expand Down
22 changes: 16 additions & 6 deletions src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
});

Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion website/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion website/docs/observers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading