@chromahq/react 0.0.1

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.
@@ -0,0 +1,39 @@
1
+ type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
2
+ interface Bridge {
3
+ send: <Req = unknown, Res = unknown>(key: string, payload?: Req) => Promise<Res>;
4
+ isConnected: boolean;
5
+ }
6
+ interface BridgeContextValue {
7
+ bridge: Bridge | null;
8
+ status: ConnectionStatus;
9
+ error: Error | null;
10
+ reconnect: () => void;
11
+ }
12
+ interface Props {
13
+ children: React.ReactNode;
14
+ retryAfter?: number;
15
+ maxRetries?: number;
16
+ onConnectionChange?: (status: ConnectionStatus) => void;
17
+ onError?: (error: Error) => void;
18
+ }
19
+ declare const BridgeProvider: React.FC<Props>;
20
+
21
+ /**
22
+ * Custom hook to access the bridge context.
23
+ * @returns The bridge context value.
24
+ */
25
+ declare const useBridge: () => BridgeContextValue;
26
+
27
+ /**
28
+ * Custom hook to send a query to the bridge and return the response.
29
+ * @param key
30
+ * @param payload
31
+ * @returns { data: Res | undefined, loading: boolean, error: unknown }
32
+ */
33
+ declare function useBridgeQuery<Res = unknown>(key: string, payload?: any): {
34
+ data: Res | undefined;
35
+ loading: boolean;
36
+ error: unknown;
37
+ };
38
+
39
+ export { BridgeProvider, useBridge, useBridgeQuery };
package/dist/index.js ADDED
@@ -0,0 +1,247 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { createContext, useState, useRef, useCallback, useEffect, useMemo, useContext } from 'react';
3
+
4
+ const BridgeContext = createContext(null);
5
+ const BridgeProvider = ({
6
+ children,
7
+ retryAfter = 1500,
8
+ maxRetries = 5,
9
+ onConnectionChange,
10
+ onError
11
+ }) => {
12
+ const [bridge, setBridge] = useState(null);
13
+ const [status, setStatus] = useState("connecting");
14
+ const [error, setError] = useState(null);
15
+ const portRef = useRef(null);
16
+ const pendingRef = useRef(
17
+ /* @__PURE__ */ new Map()
18
+ );
19
+ const uidRef = useRef(0);
20
+ const reconnectTimeoutRef = useRef(null);
21
+ const retryCountRef = useRef(0);
22
+ const isConnectingRef = useRef(false);
23
+ const errorCheckIntervalRef = useRef(null);
24
+ const updateStatus = useCallback(
25
+ (newStatus) => {
26
+ setStatus(newStatus);
27
+ onConnectionChange?.(newStatus);
28
+ },
29
+ [onConnectionChange]
30
+ );
31
+ const handleError = useCallback(
32
+ (err) => {
33
+ setError(err);
34
+ onError?.(err);
35
+ updateStatus("error");
36
+ },
37
+ [onError, updateStatus]
38
+ );
39
+ const cleanup = useCallback(() => {
40
+ if (reconnectTimeoutRef.current) {
41
+ clearTimeout(reconnectTimeoutRef.current);
42
+ reconnectTimeoutRef.current = null;
43
+ }
44
+ if (errorCheckIntervalRef.current) {
45
+ clearInterval(errorCheckIntervalRef.current);
46
+ errorCheckIntervalRef.current = null;
47
+ }
48
+ if (portRef.current) {
49
+ try {
50
+ portRef.current.disconnect();
51
+ } catch (e) {
52
+ }
53
+ portRef.current = null;
54
+ }
55
+ pendingRef.current.forEach(({ reject }) => {
56
+ reject(new Error("Bridge disconnected"));
57
+ });
58
+ pendingRef.current.clear();
59
+ setBridge(null);
60
+ isConnectingRef.current = false;
61
+ }, []);
62
+ const connect = useCallback(() => {
63
+ if (isConnectingRef.current || retryCountRef.current >= maxRetries) {
64
+ console.warn("[Bridge] Already connecting or max retries reached");
65
+ return;
66
+ }
67
+ isConnectingRef.current = true;
68
+ cleanup();
69
+ if (!chrome?.runtime?.connect) {
70
+ handleError(new Error("Chrome runtime not available"));
71
+ return;
72
+ }
73
+ try {
74
+ const port = chrome.runtime.connect({ name: "chroma-bridge" });
75
+ if (chrome.runtime.lastError) {
76
+ throw new Error(chrome.runtime.lastError.message || "Failed to connect to extension");
77
+ }
78
+ portRef.current = port;
79
+ let errorCheckCount = 0;
80
+ const maxErrorChecks = 10;
81
+ errorCheckIntervalRef.current = setInterval(() => {
82
+ errorCheckCount++;
83
+ if (chrome.runtime.lastError) {
84
+ const errorMessage = chrome.runtime.lastError.message;
85
+ console.warn("[Bridge] Runtime error detected:", errorMessage);
86
+ chrome.runtime.lastError;
87
+ if (errorCheckIntervalRef.current) {
88
+ clearInterval(errorCheckIntervalRef.current);
89
+ errorCheckIntervalRef.current = null;
90
+ }
91
+ if (errorMessage?.includes("Receiving end does not exist")) {
92
+ console.warn("[Bridge] Background script not ready, will retry connection");
93
+ cleanup();
94
+ isConnectingRef.current = false;
95
+ if (retryCountRef.current < maxRetries) {
96
+ retryCountRef.current++;
97
+ const delay = retryAfter * Math.pow(2, retryCountRef.current - 1);
98
+ reconnectTimeoutRef.current = setTimeout(connect, delay);
99
+ }
100
+ }
101
+ }
102
+ if (errorCheckCount >= maxErrorChecks) {
103
+ if (errorCheckIntervalRef.current) {
104
+ clearInterval(errorCheckIntervalRef.current);
105
+ errorCheckIntervalRef.current = null;
106
+ }
107
+ }
108
+ }, 100);
109
+ port.onMessage.addListener((msg) => {
110
+ if (msg.id && pendingRef.current.has(msg.id)) {
111
+ const { resolve } = pendingRef.current.get(msg.id);
112
+ resolve(msg.data);
113
+ pendingRef.current.delete(msg.id);
114
+ }
115
+ });
116
+ port.onDisconnect.addListener(() => {
117
+ console.warn("[Bridge] disconnected");
118
+ isConnectingRef.current = false;
119
+ if (chrome.runtime.lastError) {
120
+ handleError(
121
+ new Error(chrome.runtime.lastError.message || "Port disconnected with error")
122
+ );
123
+ chrome.runtime.lastError;
124
+ } else {
125
+ updateStatus("disconnected");
126
+ }
127
+ cleanup();
128
+ if (retryCountRef.current < maxRetries) {
129
+ retryCountRef.current++;
130
+ const delay = retryAfter * Math.pow(2, retryCountRef.current - 1);
131
+ reconnectTimeoutRef.current = setTimeout(connect, delay);
132
+ }
133
+ });
134
+ const bridgeInstance = {
135
+ send: (key, payload) => {
136
+ return new Promise((resolve, reject) => {
137
+ if (!portRef.current) {
138
+ reject(new Error("Bridge disconnected"));
139
+ return;
140
+ }
141
+ const id = `msg${uidRef.current++}`;
142
+ pendingRef.current.set(id, { resolve, reject });
143
+ const timeout = setTimeout(() => {
144
+ if (pendingRef.current.has(id)) {
145
+ pendingRef.current.delete(id);
146
+ reject(new Error("Request timeout"));
147
+ }
148
+ }, 1e4);
149
+ try {
150
+ portRef.current.postMessage({ id, key, payload });
151
+ setTimeout(() => {
152
+ if (chrome.runtime.lastError) {
153
+ const errorMessage = chrome.runtime.lastError.message;
154
+ console.warn("[Bridge] Async runtime error after postMessage:", errorMessage);
155
+ chrome.runtime.lastError;
156
+ if (pendingRef.current.has(id)) {
157
+ clearTimeout(timeout);
158
+ pendingRef.current.delete(id);
159
+ reject(new Error(errorMessage || "Async send failed"));
160
+ }
161
+ }
162
+ }, 0);
163
+ if (chrome.runtime.lastError) {
164
+ throw new Error(chrome.runtime.lastError.message || "Failed to send message");
165
+ }
166
+ } catch (e) {
167
+ clearTimeout(timeout);
168
+ pendingRef.current.delete(id);
169
+ if (chrome.runtime.lastError) {
170
+ console.warn(
171
+ "[Bridge] Runtime error during postMessage:",
172
+ chrome.runtime.lastError.message
173
+ );
174
+ chrome.runtime.lastError;
175
+ }
176
+ reject(e instanceof Error ? e : new Error("Send failed"));
177
+ }
178
+ });
179
+ },
180
+ isConnected: true
181
+ };
182
+ setBridge(bridgeInstance);
183
+ updateStatus("connected");
184
+ setError(null);
185
+ retryCountRef.current = 0;
186
+ isConnectingRef.current = false;
187
+ } catch (e) {
188
+ isConnectingRef.current = false;
189
+ const error2 = e instanceof Error ? e : new Error("Connection failed");
190
+ handleError(error2);
191
+ if (retryCountRef.current < maxRetries) {
192
+ retryCountRef.current++;
193
+ reconnectTimeoutRef.current = setTimeout(connect, retryAfter);
194
+ }
195
+ }
196
+ }, [retryAfter, maxRetries, handleError, updateStatus, cleanup]);
197
+ const reconnect = useCallback(() => {
198
+ retryCountRef.current = 0;
199
+ updateStatus("connecting");
200
+ connect();
201
+ }, [connect, updateStatus]);
202
+ useEffect(() => {
203
+ connect();
204
+ return cleanup;
205
+ }, [connect, cleanup]);
206
+ const contextValue = useMemo(
207
+ () => ({
208
+ bridge,
209
+ status,
210
+ error,
211
+ reconnect
212
+ }),
213
+ [bridge, status, error, reconnect]
214
+ );
215
+ return /* @__PURE__ */ jsx(BridgeContext.Provider, { value: contextValue, children });
216
+ };
217
+
218
+ const useBridge = () => {
219
+ const context = useContext(BridgeContext);
220
+ if (!context) {
221
+ throw new Error("useBridge must be used inside <BridgeProvider>");
222
+ }
223
+ return context;
224
+ };
225
+
226
+ function useBridgeQuery(key, payload) {
227
+ const { bridge } = useBridge();
228
+ const [data, setData] = useState();
229
+ const [loading, setLoading] = useState(true);
230
+ const [error, setError] = useState();
231
+ useEffect(() => {
232
+ let mounted = true;
233
+ setLoading(true);
234
+ if (!bridge) {
235
+ setError(new Error("Bridge is not initialized"));
236
+ setLoading(false);
237
+ return;
238
+ }
239
+ bridge.send(key, payload).then((res) => mounted && setData(res)).catch((e) => mounted && setError(e)).finally(() => mounted && setLoading(false));
240
+ return () => {
241
+ mounted = false;
242
+ };
243
+ }, [key, JSON.stringify(payload)]);
244
+ return { data, loading, error };
245
+ }
246
+
247
+ export { BridgeProvider, useBridge, useBridgeQuery };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@chromahq/react",
3
+ "version": "0.0.1",
4
+ "description": "React bindings for the Chroma Chrome extension framework",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/chromaHQ/chroma.git",
23
+ "directory": "packages/react"
24
+ },
25
+ "keywords": [
26
+ "chrome-extension",
27
+ "browser-extension",
28
+ "react",
29
+ "hooks",
30
+ "typescript"
31
+ ],
32
+ "author": "Chroma Team",
33
+ "license": "MIT",
34
+ "homepage": "https://github.com/chromaHQ/chroma#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/chromaHQ/chroma/issues"
37
+ },
38
+ "peerDependencies": {
39
+ "react": ">=18"
40
+ },
41
+ "devDependencies": {
42
+ "@rollup/plugin-node-resolve": "^15.2.3",
43
+ "@rollup/plugin-typescript": "^12.1.3",
44
+ "@types/react": "^18.2.7",
45
+ "rollup": "^4.8.0",
46
+ "rollup-plugin-dts": "^6.1.0",
47
+ "rollup-plugin-esbuild": "^6.1.0",
48
+ "typescript": "^5.6.0",
49
+ "react": "^19.1.0",
50
+ "react-dom": "^19.1.0"
51
+ },
52
+ "scripts": {
53
+ "build": "rollup -c",
54
+ "dev": "rollup -c --watch"
55
+ }
56
+ }