@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@macify/xpc",
3
- "version": "0.0.3",
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": "*",
@@ -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 xpc-sources directory */
53
+ /** Find the user's XPC sources directory */
54
54
  function findXPCSourcesDir(projectRoot) {
55
- const xpcSourcesDir = node_path.default.join(projectRoot, "modules", "xpc-service", "xpc-sources");
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 user's `XPCMessageHandler`.
4
+ /// and dispatches them to the registered functions in the service definition.
5
5
  public class MacifyServiceImpl: NSObject, MacifyXPCProtocol {
6
- private let handler: XPCMessageHandler
6
+ private let definition: XPCServiceDefinition
7
7
  private let emitter: XPCEventEmitter
8
8
 
9
- public init(handler: XPCMessageHandler, connection: NSXPCConnection) {
10
- self.handler = handler
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 user's handler
36
- let result = try await handler.handle(
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
- /// struct MyHandler: XPCMessageHandler {
10
- /// func handle(type: String, payload: [String: Any], emitter: XPCEventEmitter) async throws -> [String: Any] {
11
- /// switch type {
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
- /// XPCServiceHost.start(handler: MyHandler())
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 listener with the given message handler.
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(handler: XPCMessageHandler) {
26
- let delegate = ServiceDelegate(handler: handler)
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 handler: XPCMessageHandler
67
+ let definition: XPCServiceDefinition
43
68
 
44
- init(handler: XPCMessageHandler) {
45
- self.handler = handler
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
- handler: handler, connection: connection
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
- }