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.
- package/ios/Sources/Core/Pipe/MethodPipe+Internal.swift +101 -1
- package/ios/Sources/Core/Pipe/SparklingMethodInvocationCenter.swift +174 -0
- package/ios/Sources/Lynx/Engine/LynxPipeEngine.swift +5 -0
- package/ios/Sources/Lynx/Engine/LynxView+SPKPipe.swift +5 -1
- package/ios/Sources/Lynx/Module/SPKLynxNativeModule.swift +28 -2
- package/ios/SparklingMethod.podspec +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 =
|
|
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 ?
|
|
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
|
-
|
|
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.
|
|
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"
|