applesauce-react 0.0.0-next-20250923113611 → 0.0.0-next-20251203172109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { EventFactory } from "applesauce-factory";
1
+ import { EventFactory } from "applesauce-core";
2
2
  export declare function useEventFactory(require: false): EventFactory | undefined;
3
3
  export declare function useEventFactory(require: true): EventFactory;
4
4
  export declare function useEventFactory(): EventFactory;
@@ -1,6 +1,7 @@
1
- import { useObservableEagerState, useObservableState } from "observable-hooks";
1
+ import { useObservableEagerState } from "observable-hooks";
2
2
  import { useMemo } from "react";
3
3
  import { of } from "rxjs";
4
+ import { useObservableState } from "./use-observable-state.js";
4
5
  export function useObservableMemo(factory, deps) {
5
6
  return useObservableState(useMemo(() => factory() || of(undefined), deps));
6
7
  }
@@ -0,0 +1,20 @@
1
+ import { BehaviorSubject, Observable } from "rxjs";
2
+ /**
3
+ * A hook that subscribes to an Observable and returns its current value.
4
+ *
5
+ * Unlike the standard `useObservableState` from observable-hooks, this hook
6
+ * will synchronously get the initial value if the Observable emits synchronously.
7
+ * This prevents an extra render when the Observable has an immediate value.
8
+ *
9
+ * If the Observable does not emit synchronously, the hook returns `undefined`
10
+ * for the initial render (unlike `useObservableEagerState` which throws).
11
+ *
12
+ * The observable is only subscribed to once - the subscription created during
13
+ * the initial probe is kept alive and reused, avoiding issues with cold observables.
14
+ *
15
+ * @template TState State type.
16
+ * @param state$ An Observable of state values.
17
+ * @returns The current state value, or `undefined` if no value has been emitted yet.
18
+ */
19
+ export declare function useObservableState<TState>(state$: BehaviorSubject<TState>): TState;
20
+ export declare function useObservableState<TState>(state$: Observable<TState>): TState | undefined;
@@ -0,0 +1,107 @@
1
+ import { useDebugValue, useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { useForceUpdate } from "observable-hooks";
3
+ // Prevent React warning when using useLayoutEffect on server
4
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
5
+ // Symbol to indicate no value was emitted
6
+ const NO_VALUE = Symbol("NO_VALUE");
7
+ function createSubscription(observable) {
8
+ const subState = {
9
+ observable,
10
+ subscription: null,
11
+ latestValue: NO_VALUE,
12
+ latestError: null,
13
+ onValue: null,
14
+ onError: null,
15
+ };
16
+ subState.subscription = observable.subscribe({
17
+ next: (value) => {
18
+ subState.latestValue = value;
19
+ subState.onValue?.(value);
20
+ },
21
+ error: (error) => {
22
+ subState.latestError = error;
23
+ subState.onError?.();
24
+ },
25
+ });
26
+ return subState;
27
+ }
28
+ export function useObservableState(state$) {
29
+ const forceUpdate = useForceUpdate();
30
+ // Ref to hold the subscription state - persists across renders
31
+ const subStateRef = useRef(null);
32
+ // Initialize state - this only runs once per component instance
33
+ const [state, setState] = useState(() => {
34
+ // Clean up any existing subscription (shouldn't happen in useState init, but be safe)
35
+ if (subStateRef.current) {
36
+ subStateRef.current.subscription.unsubscribe();
37
+ }
38
+ // Create subscription and probe for sync value
39
+ const subState = createSubscription(state$);
40
+ subStateRef.current = subState;
41
+ // Return sync value if available
42
+ return subState.latestValue !== NO_VALUE ? subState.latestValue : undefined;
43
+ });
44
+ // Track current observable for staleness checks
45
+ const state$Ref = useRef(state$);
46
+ useIsomorphicLayoutEffect(() => {
47
+ state$Ref.current = state$;
48
+ });
49
+ // Handle observable changes and register callbacks
50
+ useEffect(() => {
51
+ let subState = subStateRef.current;
52
+ // If observable changed, create new subscription
53
+ if (!subState || subState.observable !== state$) {
54
+ // Clean up old subscription
55
+ subState?.subscription.unsubscribe();
56
+ // Create new subscription
57
+ subState = createSubscription(state$);
58
+ subStateRef.current = subState;
59
+ // Update state if we got a sync value from new observable
60
+ if (subState.latestValue !== NO_VALUE) {
61
+ setState(subState.latestValue);
62
+ }
63
+ else {
64
+ setState(undefined);
65
+ }
66
+ }
67
+ else {
68
+ // Same observable - check if we missed any values between useState and useEffect
69
+ if (subState.latestValue !== NO_VALUE && subState.latestValue !== state) {
70
+ setState(subState.latestValue);
71
+ }
72
+ }
73
+ // Check for errors that occurred before useEffect
74
+ if (subState.latestError !== null) {
75
+ forceUpdate();
76
+ }
77
+ // Register callbacks for future emissions
78
+ subState.onValue = (value) => {
79
+ if (state$Ref.current === state$) {
80
+ setState(value);
81
+ }
82
+ };
83
+ subState.onError = () => {
84
+ if (state$Ref.current === state$) {
85
+ forceUpdate();
86
+ }
87
+ };
88
+ return () => {
89
+ // Unregister callbacks
90
+ subState.onValue = null;
91
+ subState.onError = null;
92
+ // Unsubscribe
93
+ subState.subscription.unsubscribe();
94
+ // Clear the ref if this is still the current subscription
95
+ if (subStateRef.current === subState) {
96
+ subStateRef.current = null;
97
+ }
98
+ };
99
+ }, [state$]);
100
+ // Throw errors to be caught by error boundary
101
+ const subState = subStateRef.current;
102
+ if (subState?.latestError !== null && subState?.observable === state$) {
103
+ throw subState.latestError;
104
+ }
105
+ useDebugValue(state);
106
+ return state;
107
+ }
@@ -1 +1,2 @@
1
- export * from "observable-hooks";
1
+ export { useObservableCallback, useSubscription, useObservableEagerState, useObservableGetState, useObservablePickState, useObservableSuspense, useForceUpdate, } from "observable-hooks";
2
+ export { useObservableState } from "./use-observable-state.js";
@@ -1,2 +1,5 @@
1
1
  // NOTE: re-export hooks from observable-hooks to avoid confusion
2
- export * from "observable-hooks";
2
+ // We override useObservableState with our own implementation that gets sync values
3
+ export { useObservableCallback, useSubscription, useObservableEagerState, useObservableGetState, useObservablePickState, useObservableSuspense, useForceUpdate, } from "observable-hooks";
4
+ // Export our custom useObservableState that gets sync values
5
+ export { useObservableState } from "./use-observable-state.js";
@@ -1,7 +1,7 @@
1
1
  import { getParsedContent } from "applesauce-content/text";
2
- import { EventTemplate, NostrEvent } from "nostr-tools";
3
- import { ComponentMap } from "../helpers/nast.js";
2
+ import { EventTemplate, NostrEvent } from "applesauce-core/helpers/event";
4
3
  import { LinkRenderer } from "../helpers/build-link-renderer.js";
4
+ import { ComponentMap } from "../helpers/nast.js";
5
5
  export { ComponentMap };
6
6
  type Options = {
7
7
  /** The key to cache the results under, passing null will disable */
@@ -1,8 +1,8 @@
1
- import { useMemo } from "react";
2
1
  import { truncateContent } from "applesauce-content/nast";
3
2
  import { getParsedContent } from "applesauce-content/text";
4
- import { useRenderNast } from "./use-render-nast.js";
3
+ import { useMemo } from "react";
5
4
  import { buildLinkRenderer } from "../helpers/build-link-renderer.js";
5
+ import { useRenderNast } from "./use-render-nast.js";
6
6
  /** Returns the parsed and render text content for an event */
7
7
  export function useRenderedContent(event, components, opts) {
8
8
  // if link renderers are set, override the link components
@@ -1,4 +1,4 @@
1
- import { EventFactory } from "applesauce-factory";
1
+ import { EventFactory } from "applesauce-core";
2
2
  import { PropsWithChildren } from "react";
3
3
  export declare const FactoryContext: import("react").Context<EventFactory | undefined>;
4
4
  /** Provides an {@link EventFactory} to the component tree */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-react",
3
- "version": "0.0.0-next-20250923113611",
3
+ "version": "0.0.0-next-20251203172109",
4
4
  "description": "React hooks for applesauce",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -48,13 +48,11 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "applesauce-accounts": "0.0.0-next-20250923113611",
52
- "applesauce-actions": "0.0.0-next-20250923113611",
53
- "applesauce-content": "0.0.0-next-20250923113611",
54
- "applesauce-core": "0.0.0-next-20250923113611",
55
- "applesauce-factory": "0.0.0-next-20250923113611",
51
+ "applesauce-accounts": "0.0.0-next-20251203172109",
52
+ "applesauce-actions": "0.0.0-next-20251203172109",
53
+ "applesauce-content": "0.0.0-next-20251203172109",
54
+ "applesauce-core": "0.0.0-next-20251203172109",
56
55
  "hash-sum": "^2.0.0",
57
- "nostr-tools": "~2.17",
58
56
  "observable-hooks": "^4.2.4",
59
57
  "react": "^18.3.1",
60
58
  "rxjs": "^7.8.1"
@@ -64,7 +62,7 @@
64
62
  "@types/react": "^18.3.18",
65
63
  "rimraf": "^6.0.1",
66
64
  "typescript": "^5.8.3",
67
- "vitest": "^3.2.4"
65
+ "vitest": "^4.0.15"
68
66
  },
69
67
  "funding": {
70
68
  "type": "lightning",