sparkling-method 2.1.0-rc.24 → 2.1.0-rc.26

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.
@@ -4,10 +4,69 @@
4
4
 
5
5
  import Foundation
6
6
 
7
+ private enum MethodInvocationPayloadKeys {
8
+ static let code = "code"
9
+ static let data = "data"
10
+ static let message = "message"
11
+ static let msg = "msg"
12
+ static let containerID = "containerID"
13
+ static let protocolVersion = "protocolVersion"
14
+ }
15
+
16
+ private func methodInvocationStatusCode(_ status: MethodStatus) -> Int {
17
+ status.code == .notFound ? MethodStatusCode.unregisteredMethod.rawValue : status.rawCode
18
+ }
19
+
7
20
  extension MethodPipe {
8
21
  func executeMethod(methodName: String, params: [String: Any]?, thread: MethodThread = .mainThread, completion: CommonPipeCompletion?) {
22
+ let invocationId = UUID().uuidString
23
+ let invocationStart = Date()
24
+ let invocationCenter = SparklingMethodInvocationCenter.shared
25
+ let invocationNamespace: String? = "method-pipe"
26
+ let callbackPayload: (MethodStatus, [String: Any]?) -> [String: Any] = { status, data in
27
+ var payload: [String: Any] = [
28
+ MethodInvocationPayloadKeys.code: methodInvocationStatusCode(status),
29
+ MethodInvocationPayloadKeys.data: data ?? NSNull(),
30
+ MethodInvocationPayloadKeys.protocolVersion: "1.1.0",
31
+ ]
32
+ payload[MethodInvocationPayloadKeys.msg] = status.message ?? NSNull()
33
+ if let containerID = params?[MethodInvocationPayloadKeys.containerID] as? String {
34
+ payload[MethodInvocationPayloadKeys.containerID] = containerID
35
+ }
36
+ return payload
37
+ }
38
+
39
+ invocationCenter.notifyStart(
40
+ SparklingMethodInvocationEvent(
41
+ id: invocationId,
42
+ name: methodName,
43
+ namespace: invocationNamespace,
44
+ platform: "core",
45
+ params: params,
46
+ result: nil,
47
+ statusCode: nil,
48
+ statusMessage: nil,
49
+ startTime: invocationStart,
50
+ endTime: nil
51
+ )
52
+ )
53
+
9
54
  guard let method = self.method(forName: methodName) else {
10
- completion?(.notFound(), nil)
55
+ let status = MethodStatus.notFound()
56
+ let notFound = SparklingMethodInvocationEvent(
57
+ id: invocationId,
58
+ name: methodName,
59
+ namespace: invocationNamespace,
60
+ platform: "core",
61
+ params: params,
62
+ result: callbackPayload(status, nil),
63
+ statusCode: methodInvocationStatusCode(status),
64
+ statusMessage: status.message ?? "notFound",
65
+ startTime: invocationStart,
66
+ endTime: Date()
67
+ )
68
+ invocationCenter.notifyEnd(notFound)
69
+ completion?(status, nil)
11
70
  return
12
71
  }
13
72
 
@@ -32,10 +91,48 @@ extension MethodPipe {
32
91
  }
33
92
 
34
93
  resultDict[DictKeys.statusMessage] = fStatus.message
94
+ let callbackResult = callbackPayload(fStatus, resultDict)
95
+
96
+ invocationCenter.notifyEnd(
97
+ SparklingMethodInvocationEvent(
98
+ id: invocationId,
99
+ name: methodName,
100
+ namespace: invocationNamespace,
101
+ platform: "core",
102
+ params: params,
103
+ result: callbackResult,
104
+ statusCode: methodInvocationStatusCode(fStatus),
105
+ statusMessage: fStatus.message,
106
+ startTime: invocationStart,
107
+ endTime: Date()
108
+ )
109
+ )
110
+
35
111
  completion?(fStatus, resultDict)
36
112
  }
113
+
114
+ // Notify observers of an early-out failure (mirrors `resultBlk`).
115
+ let notifyEarlyFailure: (String) -> Void = { message in
116
+ let status = MethodStatus.invalidParameter(message: message)
117
+ invocationCenter.notifyEnd(
118
+ SparklingMethodInvocationEvent(
119
+ id: invocationId,
120
+ name: methodName,
121
+ namespace: invocationNamespace,
122
+ platform: "core",
123
+ params: params,
124
+ result: callbackPayload(status, [MethodInvocationPayloadKeys.message: message]),
125
+ statusCode: methodInvocationStatusCode(status),
126
+ statusMessage: message,
127
+ startTime: invocationStart,
128
+ endTime: Date()
129
+ )
130
+ )
131
+ }
132
+
37
133
  let paramsModelType = method.paramsModelClass
38
134
  guard var params = params else {
135
+ notifyEarlyFailure("Pipe inner error: empty params")
39
136
  completion?(.invalidParameter(message: "Pipe inner error: empty params"), nil)
40
137
  return
41
138
  }
@@ -47,6 +144,7 @@ extension MethodPipe {
47
144
  let paramsKeys: Set<String> = Set(params.keys)
48
145
  let missingKeys = requiredKeyPaths.subtracting(paramsKeys).sorted().joined(separator: ", ")
49
146
  if missingKeys.count > 0 {
147
+ notifyEarlyFailure("Missing required parameter(s): \(missingKeys)")
50
148
  completion?(.invalidParameter(message: "Missing required parameter(s): \(missingKeys)"), nil)
51
149
  return
52
150
  }
@@ -60,10 +158,12 @@ extension MethodPipe {
60
158
  throw NSError(domain: "MethodPipe", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid params model type: \(paramsModelType)"])
61
159
  }
62
160
  } catch {
161
+ notifyEarlyFailure(error.localizedDescription)
63
162
  completion?(.invalidParameter(message: error.localizedDescription), nil)
64
163
  return
65
164
  }
66
165
  guard var paramModel = paramModel else {
166
+ notifyEarlyFailure("Param model conversion failed")
67
167
  completion?(.invalidParameter(message: "Param model conversion failed: \(type(of: paramsModelType)) from dict: \(params)"), nil)
68
168
  return
69
169
  }
@@ -0,0 +1,174 @@
1
+ // Copyright (c) 2026 TikTok Pte. Ltd.
2
+ // Licensed under the Apache License Version 2.0 that can be found in the
3
+ // LICENSE file in the root directory of this source tree.
4
+
5
+ import Foundation
6
+
7
+ /// Snapshot describing one Sparkling Method invocation. The fields mirror the
8
+ /// Android `SparklingMethodInvocationCenter.Event` so debug surfaces can be
9
+ /// kept consistent across platforms.
10
+ public struct SparklingMethodInvocationEvent {
11
+ public let id: String
12
+ public let name: String
13
+ public let namespace: String?
14
+ public let platform: String
15
+ public let params: [String: Any]?
16
+ public let result: [String: Any]?
17
+ public let statusCode: Int?
18
+ public let statusMessage: String?
19
+ public let startTime: Date
20
+ public let endTime: Date?
21
+
22
+ public var durationMs: Int? {
23
+ guard let end = endTime else { return nil }
24
+ return Int((end.timeIntervalSince(startTime)) * 1000)
25
+ }
26
+ }
27
+
28
+ /// Observer protocol for [SparklingMethodInvocationCenter].
29
+ public protocol SparklingMethodInvocationObserver: AnyObject {
30
+ func sparklingMethodInvocationDidStart(_ event: SparklingMethodInvocationEvent)
31
+ func sparklingMethodInvocationDidEnd(_ event: SparklingMethodInvocationEvent)
32
+ }
33
+
34
+ public extension SparklingMethodInvocationObserver {
35
+ func sparklingMethodInvocationDidStart(_ event: SparklingMethodInvocationEvent) {}
36
+ }
37
+
38
+ /// Central hub the Sparkling Method pipe uses to broadcast every invocation
39
+ /// lifecycle event. Debug tools attach themselves at app startup; the pipe
40
+ /// only walks observers when at least one is registered.
41
+ public final class SparklingMethodInvocationCenter {
42
+ public static let shared = SparklingMethodInvocationCenter()
43
+
44
+ /// Notification posted whenever a method invocation starts. The
45
+ /// `Notification.object` is the `SparklingMethodInvocationCenter` shared
46
+ /// instance and the underlying `SparklingMethodInvocationEvent` is
47
+ /// available under `userInfo[SparklingMethodInvocationCenter.eventKey]`.
48
+ /// Debug surfaces that don't want to take a hard dependency on
49
+ /// `SparklingMethod` can subscribe by name string instead of importing
50
+ /// this module.
51
+ public static let didStartNotification = Notification.Name("SparklingMethodInvocationCenter.didStart")
52
+ public static let didEndNotification = Notification.Name("SparklingMethodInvocationCenter.didEnd")
53
+ public static let eventKey = "event"
54
+
55
+ private let lock = NSLock()
56
+ private var observers = NSHashTable<AnyObject>.weakObjects()
57
+
58
+ private init() {}
59
+
60
+ public func addObserver(_ observer: SparklingMethodInvocationObserver) {
61
+ lock.lock(); defer { lock.unlock() }
62
+ observers.add(observer as AnyObject)
63
+ }
64
+
65
+ public func removeObserver(_ observer: SparklingMethodInvocationObserver) {
66
+ lock.lock(); defer { lock.unlock() }
67
+ observers.remove(observer as AnyObject)
68
+ }
69
+
70
+ public var hasObservers: Bool {
71
+ lock.lock(); defer { lock.unlock() }
72
+ return observers.allObjects.contains { _ in true }
73
+ }
74
+
75
+ func notifyStart(_ event: SparklingMethodInvocationEvent) {
76
+ for case let observer as SparklingMethodInvocationObserver in snapshot() {
77
+ observer.sparklingMethodInvocationDidStart(event)
78
+ }
79
+ NotificationCenter.default.post(
80
+ name: SparklingMethodInvocationCenter.didStartNotification,
81
+ object: self,
82
+ userInfo: makeNotificationUserInfo(for: event)
83
+ )
84
+ }
85
+
86
+ func notifyEnd(_ event: SparklingMethodInvocationEvent) {
87
+ for case let observer as SparklingMethodInvocationObserver in snapshot() {
88
+ observer.sparklingMethodInvocationDidEnd(event)
89
+ }
90
+ NotificationCenter.default.post(
91
+ name: SparklingMethodInvocationCenter.didEndNotification,
92
+ object: self,
93
+ userInfo: makeNotificationUserInfo(for: event)
94
+ )
95
+ }
96
+
97
+ func notifyNativeToJsEvent(name: String, params: [String: Any]?, containerID: String?) {
98
+ let now = Date()
99
+ let payload: [String: Any] = [
100
+ "event": name,
101
+ "params": params ?? [:],
102
+ "containerID": containerID ?? NSNull(),
103
+ ]
104
+ let result: [String: Any] = [
105
+ "status": "sent",
106
+ "direction": "Native -> JS",
107
+ ]
108
+ notifyEnd(
109
+ SparklingMethodInvocationEvent(
110
+ id: UUID().uuidString,
111
+ name: name,
112
+ namespace: "native-to-js",
113
+ platform: "lynx",
114
+ params: payload,
115
+ result: result,
116
+ statusCode: 1,
117
+ statusMessage: "sent",
118
+ startTime: now,
119
+ endTime: now
120
+ )
121
+ )
122
+ }
123
+
124
+ func notifyCompletedInvocation(
125
+ name: String,
126
+ namespace: String?,
127
+ platform: String,
128
+ params: [String: Any]?,
129
+ result: [String: Any]?,
130
+ statusCode: Int?,
131
+ statusMessage: String?
132
+ ) {
133
+ let now = Date()
134
+ notifyEnd(
135
+ SparklingMethodInvocationEvent(
136
+ id: UUID().uuidString,
137
+ name: name,
138
+ namespace: namespace,
139
+ platform: platform,
140
+ params: params,
141
+ result: result,
142
+ statusCode: statusCode,
143
+ statusMessage: statusMessage,
144
+ startTime: now,
145
+ endTime: now
146
+ )
147
+ )
148
+ }
149
+
150
+ /// Build a stable, type-erased dictionary representation of the event so
151
+ /// debug surfaces can subscribe via `Notification.Name` strings without
152
+ /// importing this module. Keys mirror the struct field names.
153
+ private func makeNotificationUserInfo(for event: SparklingMethodInvocationEvent) -> [AnyHashable: Any] {
154
+ var info: [AnyHashable: Any] = [
155
+ SparklingMethodInvocationCenter.eventKey: event,
156
+ "id": event.id,
157
+ "name": event.name,
158
+ "platform": event.platform,
159
+ "startTime": event.startTime,
160
+ ]
161
+ if let namespace = event.namespace { info["namespace"] = namespace }
162
+ if let params = event.params { info["params"] = params }
163
+ if let result = event.result { info["result"] = result }
164
+ if let statusCode = event.statusCode { info["statusCode"] = statusCode }
165
+ if let statusMessage = event.statusMessage { info["statusMessage"] = statusMessage }
166
+ if let endTime = event.endTime { info["endTime"] = endTime }
167
+ return info
168
+ }
169
+
170
+ private func snapshot() -> [AnyObject] {
171
+ lock.lock(); defer { lock.unlock() }
172
+ return observers.allObjects
173
+ }
174
+ }
@@ -31,6 +31,11 @@ class LynxPipeEngine: PipeEngine {
31
31
  guard let lynxView = self.lynxView else {
32
32
  return
33
33
  }
34
+ SparklingMethodInvocationCenter.shared.notifyNativeToJsEvent(
35
+ name: name,
36
+ params: params,
37
+ containerID: lynxView.containerID
38
+ )
34
39
  var sendMsg = LynxSendMessage(containerID: self.lynxView?.containerID)
35
40
  sendMsg.data = params
36
41
  sendMsg.code = .succeeded
@@ -46,9 +46,13 @@ extension LynxView {
46
46
  }
47
47
  }
48
48
 
49
+ private func spk_getLynxContextIfAvailable() -> LynxContext? {
50
+ perform(#selector(getLynxContext))?.takeUnretainedValue() as? LynxContext
51
+ }
52
+
49
53
  func spk_clearModuleForDestroy() {
50
54
  if !spk_isLynxViewDestorying {
51
- if let lynxContext = self.getLynxContext() as? LynxContext {
55
+ if let lynxContext = spk_getLynxContextIfAvailable() {
52
56
  lynxContext.spk_containerID = self.containerID
53
57
  if let pipeEngine = self.spk_pipeEngine, let namescope = self.namescope {
54
58
  pipeEngine.namescope = namescope
@@ -28,11 +28,37 @@ public class SPKLynxNativeModule: NSObject, LynxModule {
28
28
 
29
29
  public func call(name: String, params: [String: Any], callback: LynxCallbackBlock?) {
30
30
  let message = LynxRecvMessage(methodName: name, rawData: params)
31
- guard let containerID = message.containerID?.isEmpty == false ? message.containerID : containerID else { return }
31
+ guard let containerID = message.containerID?.isEmpty == false ? message.containerID : containerID else {
32
+ let result = LynxSendMessage.paramsErrorMessage(
33
+ with: message.containerID,
34
+ errorMsg: "missing container id"
35
+ ).toDict()
36
+ SparklingMethodInvocationCenter.shared.notifyCompletedInvocation(
37
+ name: name,
38
+ namespace: "spkPipe",
39
+ platform: "lynx",
40
+ params: params,
41
+ result: result,
42
+ statusCode: LynxPipeStatusCode.parameterError.rawValue,
43
+ statusMessage: "missing container id"
44
+ )
45
+ callback?(result)
46
+ return
47
+ }
32
48
  guard let engine = LynxPipeEnginePool.engine(for: containerID) else {
33
49
  var errorSendMsg = LynxSendMessage.paramsErrorMessage(with: containerID, errorMsg: "error container id")
34
50
  errorSendMsg.recvMessage = message
35
- callback?(errorSendMsg.toDict())
51
+ let result = errorSendMsg.toDict()
52
+ SparklingMethodInvocationCenter.shared.notifyCompletedInvocation(
53
+ name: name,
54
+ namespace: "spkPipe",
55
+ platform: "lynx",
56
+ params: params,
57
+ result: result,
58
+ statusCode: LynxPipeStatusCode.parameterError.rawValue,
59
+ statusMessage: "error container id"
60
+ )
61
+ callback?(result)
36
62
  return
37
63
  }
38
64
 
@@ -2,7 +2,7 @@ require 'json'
2
2
 
3
3
  Pod::Spec.new do |s|
4
4
  s.name = 'SparklingMethod'
5
- s.version = "2.1.0-rc.24"
5
+ s.version = "2.1.0-rc.26"
6
6
  s.summary = "iOS SDK for Sparkling Method"
7
7
  s.description = "Core iOS method runtime for Sparkling, with Lynx integration, dependency injection support, and debug helpers."
8
8
  s.license = "Apache 2.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparkling-method",
3
- "version": "2.1.0-rc.24",
3
+ "version": "2.1.0-rc.26",
4
4
  "homepage": "https://tiktok.github.io/sparkling/",
5
5
  "repository": {
6
6
  "type": "git",