expo-http-server 0.1.14-beta.3 → 0.1.14-beta.5
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.
- package/android/src/main/java/expo/modules/httpserver/ExpoHttpServerModule.kt +88 -15
- package/build/index.d.ts +5 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +7 -5
- package/build/index.js.map +1 -1
- package/ios/ExpoHttpServerModule.swift +55 -61
- package/package.json +1 -1
- package/src/index.ts +19 -4
|
@@ -9,6 +9,8 @@ import com.safframework.server.core.http.HttpMethod
|
|
|
9
9
|
import com.safframework.server.core.http.Request
|
|
10
10
|
import com.safframework.server.core.http.Response
|
|
11
11
|
import org.json.JSONObject
|
|
12
|
+
import java.net.BindException
|
|
13
|
+
import java.util.UUID
|
|
12
14
|
import java.util.concurrent.ConcurrentHashMap
|
|
13
15
|
|
|
14
16
|
class ExpoHttpServerModule : Module() {
|
|
@@ -20,18 +22,65 @@ class ExpoHttpServerModule : Module() {
|
|
|
20
22
|
|
|
21
23
|
data class RouteConfig(val path: String, val method: String, val uuid: String)
|
|
22
24
|
|
|
25
|
+
companion object {
|
|
26
|
+
// The underlying AndroidServer/Netty bind happens on Netty's own event loop
|
|
27
|
+
// thread, asynchronously, so a BindException there can never be caught by a
|
|
28
|
+
// try/catch around server.start() - it surfaces as an uncaught exception and
|
|
29
|
+
// crashes the app. We install a single, narrowly-scoped default uncaught
|
|
30
|
+
// exception handler (chained to whatever was already registered) that only
|
|
31
|
+
// swallows BindExceptions thrown from within Netty's own call stack, so we
|
|
32
|
+
// can report the failure via onStatusUpdate instead of crashing.
|
|
33
|
+
private var bindExceptionGuardInstalled = false
|
|
34
|
+
private val statusUpdateListeners = ConcurrentHashMap.newKeySet<(String) -> Unit>()
|
|
35
|
+
|
|
36
|
+
@Synchronized
|
|
37
|
+
private fun ensureBindExceptionGuardInstalled() {
|
|
38
|
+
if (bindExceptionGuardInstalled) return
|
|
39
|
+
bindExceptionGuardInstalled = true
|
|
40
|
+
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
|
|
41
|
+
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
|
42
|
+
if (isNettyBindException(throwable)) {
|
|
43
|
+
val message = throwable.message ?: "Address already in use"
|
|
44
|
+
statusUpdateListeners.forEach { it(message) }
|
|
45
|
+
} else {
|
|
46
|
+
previousHandler?.uncaughtException(thread, throwable)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private fun isNettyBindException(throwable: Throwable): Boolean =
|
|
52
|
+
throwable is BindException &&
|
|
53
|
+
throwable.stackTrace.any { it.className.startsWith("io.netty") }
|
|
54
|
+
}
|
|
55
|
+
|
|
23
56
|
private var server: Server? = null
|
|
24
57
|
private var started = false
|
|
58
|
+
// Tracks whether the *current* server instance has actually bound a socket.
|
|
59
|
+
// A freshly built server (from setup()/buildServerWithRoutes()) holds no
|
|
60
|
+
// socket, so there's nothing to release before starting it - closing and
|
|
61
|
+
// rebuilding in that case only adds an unnecessary close/rebind race.
|
|
62
|
+
private var hasBoundSocket = false
|
|
25
63
|
private var configuredPort: Int = 0
|
|
64
|
+
private var configuredLoopbackOnly = false
|
|
26
65
|
private val routeConfigs = mutableListOf<RouteConfig>()
|
|
27
66
|
private val responses = ConcurrentHashMap<String, SimpleHttpResponse>()
|
|
67
|
+
private val onBindFailure: (String) -> Unit = { message ->
|
|
68
|
+
started = false
|
|
69
|
+
hasBoundSocket = false
|
|
70
|
+
sendEvent("onStatusUpdate", bundleOf(
|
|
71
|
+
"status" to "ERROR",
|
|
72
|
+
"message" to message
|
|
73
|
+
))
|
|
74
|
+
}
|
|
28
75
|
|
|
29
76
|
private fun handleRequest(uuid: String, request: Request, response: Response): Response {
|
|
30
77
|
val headers: Map<String, String> = request.headers()
|
|
31
78
|
val params: Map<String, String> = request.params()
|
|
32
79
|
val cookies: Map<String, String> = request.cookies().associate { it.name() to it.value() }
|
|
80
|
+
val requestId = UUID.randomUUID().toString()
|
|
33
81
|
sendEvent("onRequest", bundleOf(
|
|
34
82
|
"uuid" to uuid,
|
|
83
|
+
"requestId" to requestId,
|
|
35
84
|
"method" to request.method().name,
|
|
36
85
|
"path" to request.url(),
|
|
37
86
|
"body" to request.content(),
|
|
@@ -40,13 +89,13 @@ class ExpoHttpServerModule : Module() {
|
|
|
40
89
|
"cookiesJson" to JSONObject(cookies).toString(),
|
|
41
90
|
))
|
|
42
91
|
val deadline = System.currentTimeMillis() + 30_000
|
|
43
|
-
while (!responses.containsKey(
|
|
92
|
+
while (!responses.containsKey(requestId) && System.currentTimeMillis() < deadline) {
|
|
44
93
|
Thread.sleep(10)
|
|
45
94
|
}
|
|
46
|
-
val res = responses.remove(
|
|
95
|
+
val res = responses.remove(requestId) ?: return response
|
|
47
96
|
response.setBodyText(res.body)
|
|
48
97
|
response.setStatus(res.statusCode)
|
|
49
|
-
response.addHeader("Content-Length", "" + res.body.
|
|
98
|
+
response.addHeader("Content-Length", "" + res.body.toByteArray(Charsets.UTF_8).size)
|
|
50
99
|
response.addHeader("Content-Type", res.contentType)
|
|
51
100
|
for ((key, value) in res.headers) {
|
|
52
101
|
response.addHeader(key, value)
|
|
@@ -54,10 +103,15 @@ class ExpoHttpServerModule : Module() {
|
|
|
54
103
|
return response
|
|
55
104
|
}
|
|
56
105
|
|
|
106
|
+
private fun buildServer(): Server = AndroidServer.Builder {
|
|
107
|
+
port { configuredPort }
|
|
108
|
+
if (configuredLoopbackOnly) {
|
|
109
|
+
address { "127.0.0.1" }
|
|
110
|
+
}
|
|
111
|
+
}.build()
|
|
112
|
+
|
|
57
113
|
private fun buildServerWithRoutes(): Server {
|
|
58
|
-
var s: Server =
|
|
59
|
-
port { configuredPort }
|
|
60
|
-
}.build()
|
|
114
|
+
var s: Server = buildServer()
|
|
61
115
|
for (route in routeConfigs) {
|
|
62
116
|
s = s.request(HttpMethod.getMethod(route.method), route.path) { request: Request, response: Response ->
|
|
63
117
|
handleRequest(route.uuid, request, response)
|
|
@@ -72,14 +126,23 @@ class ExpoHttpServerModule : Module() {
|
|
|
72
126
|
|
|
73
127
|
Events("onStatusUpdate", "onRequest")
|
|
74
128
|
|
|
75
|
-
|
|
129
|
+
OnCreate {
|
|
130
|
+
ensureBindExceptionGuardInstalled()
|
|
131
|
+
statusUpdateListeners.add(onBindFailure)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
OnDestroy {
|
|
135
|
+
statusUpdateListeners.remove(onBindFailure)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Function("setup") { port: Int, loopbackOnly: Boolean ->
|
|
76
139
|
configuredPort = port
|
|
140
|
+
configuredLoopbackOnly = loopbackOnly
|
|
77
141
|
server?.close()
|
|
78
142
|
started = false
|
|
143
|
+
hasBoundSocket = false
|
|
79
144
|
routeConfigs.clear()
|
|
80
|
-
server =
|
|
81
|
-
port { configuredPort }
|
|
82
|
-
}.build()
|
|
145
|
+
server = buildServer()
|
|
83
146
|
}
|
|
84
147
|
|
|
85
148
|
Function("route") { path: String, method: String, uuid: String ->
|
|
@@ -99,12 +162,22 @@ class ExpoHttpServerModule : Module() {
|
|
|
99
162
|
if (!started) {
|
|
100
163
|
try {
|
|
101
164
|
started = true
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
165
|
+
// Only close and rebuild when the current server instance has actually
|
|
166
|
+
// bound a socket before - a freshly built instance (from setup(), or
|
|
167
|
+
// because it was never started) holds nothing to release, so closing
|
|
168
|
+
// and immediately rebinding it would just add an unnecessary race with
|
|
169
|
+
// the OS releasing the port (BindException: Address already in use).
|
|
170
|
+
if (hasBoundSocket) {
|
|
171
|
+
server?.close()
|
|
172
|
+
// Give the OS a moment to release the previous socket before rebinding
|
|
173
|
+
// to the same port - reduces (but, due to the underlying library
|
|
174
|
+
// configuring SO_REUSEADDR on the wrong socket scope, cannot fully
|
|
175
|
+
// eliminate) the "Address already in use" race.
|
|
176
|
+
Thread.sleep(300)
|
|
177
|
+
server = buildServerWithRoutes()
|
|
178
|
+
}
|
|
107
179
|
server?.start()
|
|
180
|
+
hasBoundSocket = true
|
|
108
181
|
sendEvent("onStatusUpdate", bundleOf(
|
|
109
182
|
"status" to "STARTED",
|
|
110
183
|
"message" to "Server started"
|
package/build/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface StatusEvent {
|
|
|
9
9
|
}
|
|
10
10
|
export interface RequestEvent {
|
|
11
11
|
uuid: string;
|
|
12
|
+
requestId: string;
|
|
12
13
|
method: string;
|
|
13
14
|
path: string;
|
|
14
15
|
body: string;
|
|
@@ -31,6 +32,9 @@ export interface Callback {
|
|
|
31
32
|
}
|
|
32
33
|
export declare const start: () => void;
|
|
33
34
|
export declare const route: (path: string, method: HttpMethod, callback: (request: RequestEvent) => Promise<Response>) => void;
|
|
34
|
-
export
|
|
35
|
+
export interface SetupOptions {
|
|
36
|
+
loopbackOnly?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare const setup: (port: number, onStatusUpdate?: ((event: StatusEvent) => void) | undefined, options?: SetupOptions) => void;
|
|
35
39
|
export declare const stop: () => void;
|
|
36
40
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AACvE;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxD;AAED,eAAO,MAAM,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AACvE;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxD;AAED,eAAO,MAAM,KAAK,YAqCjB,CAAC;AAEF,eAAO,MAAM,KAAK,SACV,MAAM,UACJ,UAAU,sBACE,YAAY,KAAK,QAAQ,QAAQ,CAAC,SAUvD,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,KAAK,SACV,MAAM,4BACa,WAAW,KAAK,IAAI,yBACpC,YAAY,SAUtB,CAAC;AAEF,eAAO,MAAM,IAAI,YAIhB,CAAC"}
|
package/build/index.js
CHANGED
|
@@ -9,13 +9,15 @@ export const start = () => {
|
|
|
9
9
|
try {
|
|
10
10
|
const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);
|
|
11
11
|
if (!responseHandler) {
|
|
12
|
-
ExpoHttpServerModule.respond(event.
|
|
12
|
+
ExpoHttpServerModule.respond(event.requestId, 404, "Not Found", "application/json", {}, JSON.stringify({ error: "Handler not found" }));
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
const response = await responseHandler.callback(event);
|
|
16
|
-
ExpoHttpServerModule.respond(event.
|
|
16
|
+
ExpoHttpServerModule.respond(event.requestId, response.statusCode || 200, response.statusDescription || "OK", response.contentType || "application/json", response.headers || {}, response.body || "{}");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
ExpoHttpServerModule.respond(event.requestId, 500, "Internal Server Error", "application/json", {}, JSON.stringify({ error: "Handler failed" }));
|
|
17
20
|
}
|
|
18
|
-
catch { }
|
|
19
21
|
});
|
|
20
22
|
ExpoHttpServerModule.start();
|
|
21
23
|
};
|
|
@@ -29,7 +31,7 @@ export const route = (path, method, callback) => {
|
|
|
29
31
|
});
|
|
30
32
|
ExpoHttpServerModule.route(path, method, uuid);
|
|
31
33
|
};
|
|
32
|
-
export const setup = (port, onStatusUpdate) => {
|
|
34
|
+
export const setup = (port, onStatusUpdate, options = {}) => {
|
|
33
35
|
if (onStatusUpdate) {
|
|
34
36
|
emitter.addListener("onStatusUpdate", async (event) => {
|
|
35
37
|
try {
|
|
@@ -38,7 +40,7 @@ export const setup = (port, onStatusUpdate) => {
|
|
|
38
40
|
catch { }
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
|
-
ExpoHttpServerModule.setup(port);
|
|
43
|
+
ExpoHttpServerModule.setup(port, options.loopbackOnly ?? false);
|
|
42
44
|
};
|
|
43
45
|
export const stop = () => {
|
|
44
46
|
requestSubscription?.remove();
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,oBAAoB,MAAM,wBAAwB,CAAC;AAE1D,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,oBAAoB,CAAC,CAAC;AACvD,MAAM,gBAAgB,GAAe,EAAE,CAAC;AACxC,IAAI,mBAAmB,GAAkD,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"expo-modules-core\";\n\nimport ExpoHttpServerModule from \"./ExpoHttpServerModule\";\n\nconst emitter = new EventEmitter(ExpoHttpServerModule);\nconst requestCallbacks: Callback[] = [];\nlet requestSubscription: ReturnType<typeof emitter.addListener> | null = null;\n\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"OPTIONS\";\n/**\n * PAUSED AND RESUMED are iOS only\n */\nexport type Status = \"STARTED\" | \"PAUSED\" | \"RESUMED\" | \"STOPPED\" | \"ERROR\";\n\nexport interface StatusEvent {\n status: Status;\n message: string;\n}\n\nexport interface RequestEvent {\n uuid: string;\n requestId: string;\n method: string;\n path: string;\n body: string;\n headersJson: string;\n paramsJson: string;\n cookiesJson: string;\n}\n\nexport interface Response {\n statusCode?: number;\n statusDescription?: string;\n contentType?: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface Callback {\n method: string;\n path: string;\n uuid: string;\n callback: (request: RequestEvent) => Promise<Response>;\n}\n\nexport const start = () => {\n requestSubscription?.remove();\n requestSubscription = emitter.addListener<RequestEvent>(\"onRequest\", async (event) => {\n try {\n const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);\n if (!responseHandler) {\n ExpoHttpServerModule.respond(\n event.requestId,\n 404,\n \"Not Found\",\n \"application/json\",\n {},\n JSON.stringify({ error: \"Handler not found\" }),\n );\n return;\n }\n const response = await responseHandler.callback(event);\n ExpoHttpServerModule.respond(\n event.requestId,\n response.statusCode || 200,\n response.statusDescription || \"OK\",\n response.contentType || \"application/json\",\n response.headers || {},\n response.body || \"{}\",\n );\n } catch {\n ExpoHttpServerModule.respond(\n event.requestId,\n 500,\n \"Internal Server Error\",\n \"application/json\",\n {},\n JSON.stringify({ error: \"Handler failed\" }),\n );\n }\n });\n ExpoHttpServerModule.start();\n};\n\nexport const route = (\n path: string,\n method: HttpMethod,\n callback: (request: RequestEvent) => Promise<Response>,\n) => {\n const uuid = Math.random().toString(16).slice(2);\n requestCallbacks.push({\n method,\n path,\n uuid,\n callback,\n });\n ExpoHttpServerModule.route(path, method, uuid);\n};\n\nexport interface SetupOptions {\n loopbackOnly?: boolean;\n}\n\nexport const setup = (\n port: number,\n onStatusUpdate?: (event: StatusEvent) => void,\n options: SetupOptions = {},\n) => {\n if (onStatusUpdate) {\n emitter.addListener<StatusEvent>(\"onStatusUpdate\", async (event) => {\n try {\n await onStatusUpdate(event);\n } catch {}\n });\n }\n ExpoHttpServerModule.setup(port, options.loopbackOnly ?? false);\n};\n\nexport const stop = () => {\n requestSubscription?.remove();\n requestSubscription = null;\n ExpoHttpServerModule.stop();\n};\n"]}
|
|
@@ -4,44 +4,43 @@ import Criollo
|
|
|
4
4
|
public class ExpoHttpServerModule: Module {
|
|
5
5
|
private let server = CRHTTPServer()
|
|
6
6
|
private var port: Int?
|
|
7
|
+
private var loopbackOnly = false
|
|
7
8
|
private var stopped = false
|
|
8
|
-
private var
|
|
9
|
+
private var isListening = false
|
|
10
|
+
private var foregroundObserver: NSObjectProtocol?
|
|
9
11
|
private var responses = [String: CRResponse]()
|
|
10
12
|
private var bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
|
|
13
14
|
public func definition() -> ModuleDefinition {
|
|
14
15
|
Name("ExpoHttpServer")
|
|
15
16
|
|
|
16
17
|
Events("onStatusUpdate", "onRequest")
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
Function("setup", setupHandler)
|
|
19
20
|
Function("start", startHandler)
|
|
20
21
|
Function("route", routeHandler)
|
|
21
22
|
Function("respond", respondHandler)
|
|
22
23
|
Function("stop", stopHandler)
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
private func setupHandler(port: Int) {
|
|
25
|
+
|
|
26
|
+
private func setupHandler(port: Int, loopbackOnly: Bool) {
|
|
26
27
|
self.port = port;
|
|
28
|
+
self.loopbackOnly = loopbackOnly;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
private func startHandler() {
|
|
30
|
-
if
|
|
31
|
-
NotificationCenter.default.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
) { [weak self] _ in
|
|
38
|
-
guard let self, !self.stopped else { return }
|
|
39
|
-
self.startServer(status: "RESUMED", message: "Server resumed")
|
|
32
|
+
if (foregroundObserver == nil) {
|
|
33
|
+
foregroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
|
|
34
|
+
guard let self = self, !self.stopped, !self.isListening else {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
self.startServer(status: "RESUMED", message: "Server resumed")
|
|
38
|
+
}
|
|
40
39
|
}
|
|
41
|
-
stopped = false
|
|
40
|
+
stopped = false;
|
|
42
41
|
startServer(status: "STARTED", message: "Server started")
|
|
43
42
|
}
|
|
44
|
-
|
|
43
|
+
|
|
45
44
|
private func routeHandler(path: String, method: String, uuid: String) {
|
|
46
45
|
server.add(path, block: { (req, res, next) in
|
|
47
46
|
DispatchQueue.main.async {
|
|
@@ -49,9 +48,11 @@ public class ExpoHttpServerModule: Module {
|
|
|
49
48
|
if let body = req.body, let bodyData = try? JSONSerialization.data(withJSONObject: body) {
|
|
50
49
|
bodyString = String(data: bodyData, encoding: .utf8) ?? "{}"
|
|
51
50
|
}
|
|
52
|
-
|
|
51
|
+
let requestId = UUID().uuidString
|
|
52
|
+
self.responses[requestId] = res
|
|
53
53
|
self.sendEvent("onRequest", [
|
|
54
54
|
"uuid": uuid,
|
|
55
|
+
"requestId": requestId,
|
|
55
56
|
"method": req.method.toString(),
|
|
56
57
|
"path": path,
|
|
57
58
|
"body": bodyString,
|
|
@@ -62,77 +63,66 @@ public class ExpoHttpServerModule: Module {
|
|
|
62
63
|
}
|
|
63
64
|
}, recursive: false, method: CRHTTPMethod.fromString(method))
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
-
private func respondHandler(
|
|
66
|
+
|
|
67
|
+
private func respondHandler(requestId: String,
|
|
67
68
|
statusCode: Int,
|
|
68
69
|
statusDescription: String,
|
|
69
70
|
contentType: String,
|
|
70
71
|
headers: [String: String],
|
|
71
72
|
body: String) {
|
|
72
73
|
DispatchQueue.main.async {
|
|
73
|
-
if let response = self.responses[
|
|
74
|
+
if let response = self.responses[requestId] {
|
|
74
75
|
response.setStatusCode(UInt(statusCode), description: statusDescription)
|
|
75
76
|
response.setValue(contentType, forHTTPHeaderField: "Content-type")
|
|
76
|
-
response.setValue("\(body.count)", forHTTPHeaderField: "Content-Length")
|
|
77
|
+
response.setValue("\(body.utf8.count)", forHTTPHeaderField: "Content-Length")
|
|
77
78
|
for (key, value) in headers {
|
|
78
79
|
response.setValue(value, forHTTPHeaderField: key)
|
|
79
80
|
}
|
|
80
81
|
response.send(body);
|
|
81
|
-
self.responses[
|
|
82
|
+
self.responses[requestId] = nil;
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
|
|
84
86
|
}
|
|
85
|
-
|
|
87
|
+
|
|
86
88
|
private func stopHandler() {
|
|
87
|
-
stopped = true
|
|
88
|
-
if let
|
|
89
|
-
NotificationCenter.default.removeObserver(
|
|
89
|
+
stopped = true;
|
|
90
|
+
if let observer = foregroundObserver {
|
|
91
|
+
NotificationCenter.default.removeObserver(observer)
|
|
90
92
|
foregroundObserver = nil
|
|
91
93
|
}
|
|
92
94
|
stopServer(status: "STOPPED", message: "Server stopped")
|
|
93
95
|
}
|
|
94
|
-
|
|
96
|
+
|
|
95
97
|
private func startServer(status: String, message: String) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
stopServer()
|
|
99
|
+
if let port = port {
|
|
100
|
+
var error: NSError?
|
|
101
|
+
server.startListening(&error, portNumber: UInt(port), interface: loopbackOnly ? "localhost" : nil)
|
|
102
|
+
if (error != nil) {
|
|
103
|
+
sendEvent("onStatusUpdate", [
|
|
104
|
+
"status": "ERROR",
|
|
105
|
+
"message": error?.localizedDescription ?? "Unknown error starting server"
|
|
106
|
+
])
|
|
107
|
+
} else {
|
|
108
|
+
isListening = true
|
|
109
|
+
beginBackgroundTask()
|
|
110
|
+
sendEvent("onStatusUpdate", [
|
|
111
|
+
"status": status,
|
|
112
|
+
"message": message
|
|
113
|
+
])
|
|
102
114
|
}
|
|
103
115
|
} else {
|
|
104
|
-
doStart(status: status, message: message)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private func doStart(status: String, message: String) {
|
|
109
|
-
guard let port = port else {
|
|
110
116
|
sendEvent("onStatusUpdate", [
|
|
111
117
|
"status": "ERROR",
|
|
112
118
|
"message": "Can't start server with port configured"
|
|
113
119
|
])
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
var error: NSError?
|
|
117
|
-
server.startListening(&error, portNumber: UInt(port))
|
|
118
|
-
if let error = error {
|
|
119
|
-
sendEvent("onStatusUpdate", [
|
|
120
|
-
"status": "ERROR",
|
|
121
|
-
"message": error.localizedDescription
|
|
122
|
-
])
|
|
123
|
-
} else {
|
|
124
|
-
isRunning = true
|
|
125
|
-
beginBackgroundTask()
|
|
126
|
-
sendEvent("onStatusUpdate", [
|
|
127
|
-
"status": status,
|
|
128
|
-
"message": message
|
|
129
|
-
])
|
|
130
120
|
}
|
|
131
121
|
}
|
|
132
|
-
|
|
122
|
+
|
|
133
123
|
private func stopServer(status: String? = nil, message: String? = nil) {
|
|
134
124
|
server.stopListening()
|
|
135
|
-
|
|
125
|
+
isListening = false
|
|
136
126
|
endBackgroundTask()
|
|
137
127
|
if let status = status, let message = message {
|
|
138
128
|
sendEvent("onStatusUpdate", [
|
|
@@ -141,7 +131,7 @@ public class ExpoHttpServerModule: Module {
|
|
|
141
131
|
])
|
|
142
132
|
}
|
|
143
133
|
}
|
|
144
|
-
|
|
134
|
+
|
|
145
135
|
private func beginBackgroundTask() {
|
|
146
136
|
if (bgTaskIdentifier == UIBackgroundTaskIdentifier.invalid) {
|
|
147
137
|
self.bgTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "BgTask", expirationHandler: {
|
|
@@ -176,11 +166,13 @@ extension CRHTTPMethod {
|
|
|
176
166
|
return "PUT"
|
|
177
167
|
case .delete:
|
|
178
168
|
return "DELETE"
|
|
169
|
+
case .options:
|
|
170
|
+
return "OPTIONS"
|
|
179
171
|
default:
|
|
180
172
|
return "GET"
|
|
181
173
|
}
|
|
182
174
|
}
|
|
183
|
-
|
|
175
|
+
|
|
184
176
|
static func fromString(_ string: String) -> Self {
|
|
185
177
|
var httpMethod: CRHTTPMethod
|
|
186
178
|
switch (string) {
|
|
@@ -190,6 +182,8 @@ extension CRHTTPMethod {
|
|
|
190
182
|
httpMethod = .put
|
|
191
183
|
case "DELETE":
|
|
192
184
|
httpMethod = .delete
|
|
185
|
+
case "OPTIONS":
|
|
186
|
+
httpMethod = .options
|
|
193
187
|
default:
|
|
194
188
|
httpMethod = .get
|
|
195
189
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface StatusEvent {
|
|
|
19
19
|
|
|
20
20
|
export interface RequestEvent {
|
|
21
21
|
uuid: string;
|
|
22
|
+
requestId: string;
|
|
22
23
|
method: string;
|
|
23
24
|
path: string;
|
|
24
25
|
body: string;
|
|
@@ -49,7 +50,7 @@ export const start = () => {
|
|
|
49
50
|
const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);
|
|
50
51
|
if (!responseHandler) {
|
|
51
52
|
ExpoHttpServerModule.respond(
|
|
52
|
-
event.
|
|
53
|
+
event.requestId,
|
|
53
54
|
404,
|
|
54
55
|
"Not Found",
|
|
55
56
|
"application/json",
|
|
@@ -60,14 +61,23 @@ export const start = () => {
|
|
|
60
61
|
}
|
|
61
62
|
const response = await responseHandler.callback(event);
|
|
62
63
|
ExpoHttpServerModule.respond(
|
|
63
|
-
event.
|
|
64
|
+
event.requestId,
|
|
64
65
|
response.statusCode || 200,
|
|
65
66
|
response.statusDescription || "OK",
|
|
66
67
|
response.contentType || "application/json",
|
|
67
68
|
response.headers || {},
|
|
68
69
|
response.body || "{}",
|
|
69
70
|
);
|
|
70
|
-
} catch {
|
|
71
|
+
} catch {
|
|
72
|
+
ExpoHttpServerModule.respond(
|
|
73
|
+
event.requestId,
|
|
74
|
+
500,
|
|
75
|
+
"Internal Server Error",
|
|
76
|
+
"application/json",
|
|
77
|
+
{},
|
|
78
|
+
JSON.stringify({ error: "Handler failed" }),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
71
81
|
});
|
|
72
82
|
ExpoHttpServerModule.start();
|
|
73
83
|
};
|
|
@@ -87,9 +97,14 @@ export const route = (
|
|
|
87
97
|
ExpoHttpServerModule.route(path, method, uuid);
|
|
88
98
|
};
|
|
89
99
|
|
|
100
|
+
export interface SetupOptions {
|
|
101
|
+
loopbackOnly?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
export const setup = (
|
|
91
105
|
port: number,
|
|
92
106
|
onStatusUpdate?: (event: StatusEvent) => void,
|
|
107
|
+
options: SetupOptions = {},
|
|
93
108
|
) => {
|
|
94
109
|
if (onStatusUpdate) {
|
|
95
110
|
emitter.addListener<StatusEvent>("onStatusUpdate", async (event) => {
|
|
@@ -98,7 +113,7 @@ export const setup = (
|
|
|
98
113
|
} catch {}
|
|
99
114
|
});
|
|
100
115
|
}
|
|
101
|
-
ExpoHttpServerModule.setup(port);
|
|
116
|
+
ExpoHttpServerModule.setup(port, options.loopbackOnly ?? false);
|
|
102
117
|
};
|
|
103
118
|
|
|
104
119
|
export const stop = () => {
|