@macify/xpc 0.0.3 → 0.0.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/package.json +3 -2
- package/plugin/build/index.cjs +3 -3
- package/swift/Sources/MacifyXPCService/FunctionDefinition.swift +54 -0
- package/swift/Sources/MacifyXPCService/MacifyServiceImpl.swift +6 -6
- package/swift/Sources/MacifyXPCService/XPCServiceDefinitionBuilder.swift +67 -0
- package/swift/Sources/MacifyXPCService/XPCServiceHost.swift +44 -17
- package/swift/Sources/MacifyXPCService/XPCMessageHandler.swift +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@macify/xpc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "XPC service support for Mac Catalyst Expo apps",
|
|
5
5
|
"main": "build/index.cjs",
|
|
6
6
|
"types": "build/index.d.cts",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@expo/config-plugins": "*",
|
|
22
22
|
"@macify/xcode-types": "workspace:*",
|
|
23
|
-
"tsdown": "*"
|
|
23
|
+
"tsdown": "*",
|
|
24
|
+
"typescript": "*"
|
|
24
25
|
},
|
|
25
26
|
"peerDependencies": {
|
|
26
27
|
"expo-modules-core": "*",
|
package/plugin/build/index.cjs
CHANGED
|
@@ -50,9 +50,9 @@ function resolveSwiftPackagePath() {
|
|
|
50
50
|
if (!node_fs.default.existsSync(swiftDir)) throw new Error(`[xpc] Could not find Swift package at ${swiftDir}. Is @macify/xpc installed?`);
|
|
51
51
|
return swiftDir;
|
|
52
52
|
}
|
|
53
|
-
/** Find the user's
|
|
53
|
+
/** Find the user's XPC sources directory */
|
|
54
54
|
function findXPCSourcesDir(projectRoot) {
|
|
55
|
-
const xpcSourcesDir = node_path.default.join(projectRoot, "
|
|
55
|
+
const xpcSourcesDir = node_path.default.join(projectRoot, "macify", "xpc");
|
|
56
56
|
if (!node_fs.default.existsSync(xpcSourcesDir)) throw new Error(`[xpc] Could not find XPC sources at ${xpcSourcesDir}. Run 'npx macx init' first.`);
|
|
57
57
|
return xpcSourcesDir;
|
|
58
58
|
}
|
|
@@ -95,7 +95,7 @@ const withXPCService = (config, props = {}) => {
|
|
|
95
95
|
const spmRefSection = objects["XCLocalSwiftPackageReference"] || (objects["XCLocalSwiftPackageReference"] = {});
|
|
96
96
|
spmRefSection[spmRefUuid] = {
|
|
97
97
|
isa: "XCLocalSwiftPackageReference",
|
|
98
|
-
relativePath: relativeSwiftPath
|
|
98
|
+
relativePath: `"${relativeSwiftPath}"`
|
|
99
99
|
};
|
|
100
100
|
spmRefSection[`${spmRefUuid}_comment`] = "XCLocalSwiftPackageReference \"MacifyXPCService\"";
|
|
101
101
|
const projectSection = project.pbxProjectSection();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Represents a registered XPC function with a name and handler closure.
|
|
4
|
+
public struct FunctionDefinition {
|
|
5
|
+
let name: String
|
|
6
|
+
let handler: ([String: Any], XPCEventEmitter) async throws -> [String: Any]
|
|
7
|
+
|
|
8
|
+
init(
|
|
9
|
+
name: String,
|
|
10
|
+
handler: @escaping ([String: Any], XPCEventEmitter) async throws -> [String: Any]
|
|
11
|
+
) {
|
|
12
|
+
self.name = name
|
|
13
|
+
self.handler = handler
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Register an XPC function with no arguments.
|
|
18
|
+
/// The handler receives no payload or emitter.
|
|
19
|
+
/// - Parameters:
|
|
20
|
+
/// - name: The function name exposed to the app
|
|
21
|
+
/// - handler: An async closure that returns a response dictionary
|
|
22
|
+
/// - Returns: A `FunctionDefinition` to be collected by the result builder
|
|
23
|
+
public func Function(
|
|
24
|
+
_ name: String,
|
|
25
|
+
handler: @escaping () async throws -> [String: Any]
|
|
26
|
+
) -> FunctionDefinition {
|
|
27
|
+
FunctionDefinition(name: name) { _, _ in try await handler() }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Register an XPC function with a payload argument.
|
|
31
|
+
/// The handler receives the decoded JSON payload.
|
|
32
|
+
/// - Parameters:
|
|
33
|
+
/// - name: The function name exposed to the app
|
|
34
|
+
/// - handler: An async closure that takes the payload and returns a response dictionary
|
|
35
|
+
/// - Returns: A `FunctionDefinition` to be collected by the result builder
|
|
36
|
+
public func Function(
|
|
37
|
+
_ name: String,
|
|
38
|
+
handler: @escaping ([String: Any]) async throws -> [String: Any]
|
|
39
|
+
) -> FunctionDefinition {
|
|
40
|
+
FunctionDefinition(name: name) { payload, _ in try await handler(payload) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Register an XPC function with payload and emitter arguments.
|
|
44
|
+
/// The handler receives the decoded JSON payload and an event emitter for pushing events.
|
|
45
|
+
/// - Parameters:
|
|
46
|
+
/// - name: The function name exposed to the app
|
|
47
|
+
/// - handler: An async closure that takes the payload and emitter, and returns a response dictionary
|
|
48
|
+
/// - Returns: A `FunctionDefinition` to be collected by the result builder
|
|
49
|
+
public func Function(
|
|
50
|
+
_ name: String,
|
|
51
|
+
handler: @escaping ([String: Any], XPCEventEmitter) async throws -> [String: Any]
|
|
52
|
+
) -> FunctionDefinition {
|
|
53
|
+
FunctionDefinition(name: name, handler: handler)
|
|
54
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
/// The XPC service implementation that receives generic JSON RPC calls
|
|
4
|
-
/// and dispatches them to the
|
|
4
|
+
/// and dispatches them to the registered functions in the service definition.
|
|
5
5
|
public class MacifyServiceImpl: NSObject, MacifyXPCProtocol {
|
|
6
|
-
private let
|
|
6
|
+
private let definition: XPCServiceDefinition
|
|
7
7
|
private let emitter: XPCEventEmitter
|
|
8
8
|
|
|
9
|
-
public init(
|
|
10
|
-
self.
|
|
9
|
+
public init(definition: XPCServiceDefinition, connection: NSXPCConnection) {
|
|
10
|
+
self.definition = definition
|
|
11
11
|
self.emitter = XPCEventEmitter(connection: connection)
|
|
12
12
|
super.init()
|
|
13
13
|
}
|
|
@@ -32,8 +32,8 @@ public class MacifyServiceImpl: NSObject, MacifyXPCProtocol {
|
|
|
32
32
|
decoded = json
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Dispatch to
|
|
36
|
-
let result = try await
|
|
35
|
+
// Dispatch to the appropriate function in the definition
|
|
36
|
+
let result = try await definition.call(
|
|
37
37
|
type: type, payload: decoded, emitter: emitter
|
|
38
38
|
)
|
|
39
39
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Holds a collection of registered XPC functions and dispatches calls to them.
|
|
4
|
+
public struct XPCServiceDefinition {
|
|
5
|
+
private let functionsByName: [String: ([String: Any], XPCEventEmitter) async throws -> [String: Any]]
|
|
6
|
+
|
|
7
|
+
init(functions: [FunctionDefinition]) {
|
|
8
|
+
var dict: [String: ([String: Any], XPCEventEmitter) async throws -> [String: Any]] = [:]
|
|
9
|
+
for function in functions {
|
|
10
|
+
dict[function.name] = function.handler
|
|
11
|
+
}
|
|
12
|
+
self.functionsByName = dict
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Look up and call a function by name.
|
|
16
|
+
/// - Parameters:
|
|
17
|
+
/// - type: The function name
|
|
18
|
+
/// - payload: The decoded JSON payload
|
|
19
|
+
/// - emitter: The event emitter for pushing events back to the app
|
|
20
|
+
/// - Returns: The response dictionary
|
|
21
|
+
/// - Throws: `MacifyXPCError.unknownMethod` if the function name is not registered
|
|
22
|
+
func call(
|
|
23
|
+
type: String,
|
|
24
|
+
payload: [String: Any],
|
|
25
|
+
emitter: XPCEventEmitter
|
|
26
|
+
) async throws -> [String: Any] {
|
|
27
|
+
guard let handler = functionsByName[type] else {
|
|
28
|
+
throw MacifyXPCError.unknownMethod(type)
|
|
29
|
+
}
|
|
30
|
+
return try await handler(payload, emitter)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Result builder for composing XPC function definitions.
|
|
35
|
+
@resultBuilder
|
|
36
|
+
public struct XPCServiceDefinitionBuilder {
|
|
37
|
+
/// Build a single function definition.
|
|
38
|
+
public static func buildExpression(_ expression: FunctionDefinition) -> [FunctionDefinition] {
|
|
39
|
+
[expression]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Build a block of function definitions.
|
|
43
|
+
public static func buildBlock(_ components: [FunctionDefinition]...) -> [FunctionDefinition] {
|
|
44
|
+
components.flatMap { $0 }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Handle optional components (e.g., `if` without `else`).
|
|
48
|
+
public static func buildOptional(_ component: [FunctionDefinition]?) -> [FunctionDefinition] {
|
|
49
|
+
component ?? []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Handle the first branch of a conditional.
|
|
53
|
+
public static func buildEither(first component: [FunctionDefinition]) -> [FunctionDefinition] {
|
|
54
|
+
component
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Handle the second branch of a conditional.
|
|
58
|
+
public static func buildEither(second component: [FunctionDefinition]) -> [FunctionDefinition] {
|
|
59
|
+
component
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Handle `if #available(...)` checks.
|
|
63
|
+
@available(macOS 13.0, *)
|
|
64
|
+
public static func buildLimitedAvailability(_ component: [FunctionDefinition]) -> [FunctionDefinition] {
|
|
65
|
+
component
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -6,24 +6,23 @@ import Foundation
|
|
|
6
6
|
/// ```swift
|
|
7
7
|
/// import MacifyXPCService
|
|
8
8
|
///
|
|
9
|
-
///
|
|
10
|
-
///
|
|
11
|
-
///
|
|
12
|
-
/// case "ping":
|
|
13
|
-
/// return ["message": "pong"]
|
|
14
|
-
/// default:
|
|
15
|
-
/// throw MacifyXPCError.unknownMethod(type)
|
|
16
|
-
/// }
|
|
9
|
+
/// XPCService {
|
|
10
|
+
/// Function("ping") {
|
|
11
|
+
/// return ["message": "pong"]
|
|
17
12
|
/// }
|
|
18
|
-
/// }
|
|
19
13
|
///
|
|
20
|
-
///
|
|
14
|
+
/// Function("processImage") { (payload, emitter) in
|
|
15
|
+
/// let path = payload["path"] as! String
|
|
16
|
+
/// emitter.emit("progress", payload: ["percent": 0.5])
|
|
17
|
+
/// return ["result": "processed \(path)"]
|
|
18
|
+
/// }
|
|
19
|
+
/// }
|
|
21
20
|
/// ```
|
|
22
21
|
public enum XPCServiceHost {
|
|
23
|
-
/// Start the XPC service
|
|
22
|
+
/// Start the XPC service with the given definition.
|
|
24
23
|
/// This function never returns — it runs the main run loop.
|
|
25
|
-
public static func start(
|
|
26
|
-
let delegate = ServiceDelegate(
|
|
24
|
+
public static func start(definition: XPCServiceDefinition) {
|
|
25
|
+
let delegate = ServiceDelegate(definition: definition)
|
|
27
26
|
let listener = NSXPCListener.service()
|
|
28
27
|
listener.delegate = delegate
|
|
29
28
|
|
|
@@ -37,12 +36,38 @@ public enum XPCServiceHost {
|
|
|
37
36
|
private static var _delegate: ServiceDelegate?
|
|
38
37
|
}
|
|
39
38
|
|
|
39
|
+
/// Entry point DSL for defining and starting an XPC service.
|
|
40
|
+
///
|
|
41
|
+
/// Usage:
|
|
42
|
+
/// ```swift
|
|
43
|
+
/// XPCService {
|
|
44
|
+
/// Function("ping") {
|
|
45
|
+
/// return ["message": "pong"]
|
|
46
|
+
/// }
|
|
47
|
+
///
|
|
48
|
+
/// Function("echo") { (payload) in
|
|
49
|
+
/// return payload
|
|
50
|
+
/// }
|
|
51
|
+
/// }
|
|
52
|
+
/// ```
|
|
53
|
+
/// - Parameter content: A builder closure that defines the service functions
|
|
54
|
+
public func XPCService(
|
|
55
|
+
@XPCServiceDefinitionBuilder _ content: () -> [FunctionDefinition]
|
|
56
|
+
) {
|
|
57
|
+
let functions = content()
|
|
58
|
+
let definition = XPCServiceDefinition(functions: functions)
|
|
59
|
+
XPCServiceHost.start(definition: definition)
|
|
60
|
+
// Keep the process alive indefinitely
|
|
61
|
+
dispatchMain()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
40
65
|
/// Internal delegate that vends `MacifyServiceImpl` instances to incoming connections.
|
|
41
66
|
class ServiceDelegate: NSObject, NSXPCListenerDelegate {
|
|
42
|
-
let
|
|
67
|
+
let definition: XPCServiceDefinition
|
|
43
68
|
|
|
44
|
-
init(
|
|
45
|
-
self.
|
|
69
|
+
init(definition: XPCServiceDefinition) {
|
|
70
|
+
self.definition = definition
|
|
46
71
|
super.init()
|
|
47
72
|
}
|
|
48
73
|
|
|
@@ -62,7 +87,7 @@ class ServiceDelegate: NSObject, NSXPCListenerDelegate {
|
|
|
62
87
|
|
|
63
88
|
// Vend the service implementation
|
|
64
89
|
connection.exportedObject = MacifyServiceImpl(
|
|
65
|
-
|
|
90
|
+
definition: definition, connection: connection
|
|
66
91
|
)
|
|
67
92
|
|
|
68
93
|
connection.invalidationHandler = {
|
|
@@ -73,3 +98,5 @@ class ServiceDelegate: NSObject, NSXPCListenerDelegate {
|
|
|
73
98
|
return true
|
|
74
99
|
}
|
|
75
100
|
}
|
|
101
|
+
|
|
102
|
+
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
/// Protocol that users implement to handle XPC messages.
|
|
4
|
-
/// The user's `main.swift` creates a struct conforming to this and passes
|
|
5
|
-
/// it to `XPCServiceHost.start(handler:)`.
|
|
6
|
-
public protocol XPCMessageHandler {
|
|
7
|
-
/// Handle an incoming RPC call.
|
|
8
|
-
/// - Parameters:
|
|
9
|
-
/// - type: The message type string (e.g. "ping", "processImage")
|
|
10
|
-
/// - payload: Decoded JSON payload as a dictionary
|
|
11
|
-
/// - emitter: Used to push events back to the app
|
|
12
|
-
/// - Returns: A dictionary that will be JSON-encoded and sent back as the reply
|
|
13
|
-
func handle(
|
|
14
|
-
type: String,
|
|
15
|
-
payload: [String: Any],
|
|
16
|
-
emitter: XPCEventEmitter
|
|
17
|
-
) async throws -> [String: Any]
|
|
18
|
-
}
|