@npaw/forge-react-native 9.6.1

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.
Files changed (74) hide show
  1. package/LICENSE +10 -0
  2. package/NPAWForgeReactNative.podspec +44 -0
  3. package/README.md +51 -0
  4. package/android/build.gradle +77 -0
  5. package/android/src/main/AndroidManifest.xml +14 -0
  6. package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +295 -0
  7. package/android/src/main/java/com/npaw/forge/reactnative/ForgeInstanceRegistry.java +61 -0
  8. package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +735 -0
  9. package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgePackage.java +53 -0
  10. package/android/src/test/java/com/npaw/forge/reactnative/ForgeDtoMapperTest.java +133 -0
  11. package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +82 -0
  12. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/ForgeKernelFFI.h +17 -0
  13. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +873 -0
  14. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/module.modulemap +7 -0
  15. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
  16. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/ForgeKernelFFI.h +17 -0
  17. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +873 -0
  18. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/module.modulemap +7 -0
  19. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
  20. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/ForgeKernelFFI.h +17 -0
  21. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +873 -0
  22. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/module.modulemap +7 -0
  23. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
  24. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/ForgeKernelFFI.h +17 -0
  25. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +873 -0
  26. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/module.modulemap +7 -0
  27. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
  28. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +3931 -0
  29. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +873 -0
  30. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.modulemap +7 -0
  31. package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAccessLogFacts.swift +152 -0
  32. package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +877 -0
  33. package/ios/ForgeApple/Sources/NPAWForgeCapture/AttributionCapture.swift +158 -0
  34. package/ios/ForgeApple/Sources/NPAWForgeCapture/CapturePlanGate.swift +189 -0
  35. package/ios/ForgeApple/Sources/NPAWForgeCapture/CrashCapture.swift +120 -0
  36. package/ios/ForgeApple/Sources/NPAWForgeCapture/ForgeAutomaticCapture.swift +639 -0
  37. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsCapture.swift +1176 -0
  38. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsEngine.swift +674 -0
  39. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsMarkers.swift +105 -0
  40. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsSwiftUI.swift +284 -0
  41. package/ios/ForgeApple/Sources/NPAWForgeCapture/SwiftUICapture.swift +44 -0
  42. package/ios/ForgeApple/Sources/NPAWForgeCore/AdAdapter.swift +199 -0
  43. package/ios/ForgeApple/Sources/NPAWForgeCore/AdSemantics.swift +136 -0
  44. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +596 -0
  45. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +297 -0
  46. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeClock.swift +29 -0
  47. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeLogging.swift +109 -0
  48. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeOutcome.swift +89 -0
  49. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +291 -0
  50. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeStorage.swift +70 -0
  51. package/ios/ForgeApple/Sources/NPAWForgeCore/ManualAdReporter.swift +180 -0
  52. package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +453 -0
  53. package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +594 -0
  54. package/ios/ForgeApple/Sources/NPAWForgeCore/NetworkRequestRecord.swift +191 -0
  55. package/ios/ForgeApple/Sources/NPAWForgeCore/ObjectiveCWrappers.swift +191 -0
  56. package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerAdapter.swift +299 -0
  57. package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerSemantics.swift +266 -0
  58. package/ios/ForgeApple/Sources/NPAWForgeCore/PrivacySanitizer.swift +59 -0
  59. package/ios/ForgeApple/Sources/NPAWForgeCore/VideoAdapter.swift +478 -0
  60. package/ios/ForgeApple/Sources/NPAWForgeCore/VideoOptions.swift +431 -0
  61. package/ios/ForgeApple/Sources/NPAWForgeNetworkCapture/ForgeNetworkCapture.swift +116 -0
  62. package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/NPAWForgeNetworkSwizzler.m +293 -0
  63. package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/include/NPAWForgeNetworkSwizzler.h +33 -0
  64. package/ios/ForgeModuleImpl.swift +1131 -0
  65. package/ios/NPAWForgeTurboModule.mm +399 -0
  66. package/package.json +33 -0
  67. package/src/ForgeAdReporter.ts +101 -0
  68. package/src/ForgeVideo.ts +169 -0
  69. package/src/NPAWForge.ts +177 -0
  70. package/src/capture.ts +192 -0
  71. package/src/events.ts +40 -0
  72. package/src/index.ts +37 -0
  73. package/src/specs/NativeNPAWForge.ts +168 -0
  74. package/src/types.ts +114 -0
@@ -0,0 +1,1131 @@
1
+ /**
2
+ * Copyright (C) NPAW - All Rights Reserved
3
+ *
4
+ * This source code is protected under international copyright law. All rights
5
+ * reserved and protected by the copyright holders.
6
+ * This file is confidential and only available to authorized individuals with the
7
+ * permission of the copyright holders. If you encounter this file and do not have
8
+ * permission, please contact the copyright holders and delete this file.
9
+ *
10
+ *
11
+ * @author: Victor Soto <victor.soto@nicepeopleatwork.com>
12
+ * @description: Swift implementation behind the core Forge TurboModule - full facade surface over the Swift NPAWForge facade (sessions, identity, raw/business, network, diagnostics, video fire*, ad reporters, kernel event feed).
13
+ */
14
+
15
+ import Foundation
16
+ #if canImport(NPAWForgeCore)
17
+ import NPAWForgeCore
18
+ #elseif canImport(NPAWForgeApple)
19
+ import NPAWForgeApple
20
+ #endif
21
+ #if canImport(NPAWForgeNetworkCapture)
22
+ import NPAWForgeNetworkCapture
23
+ #endif
24
+
25
+ /// Bridge-friendly resolve/reject shapes so this file never imports React.
26
+ public typealias ForgeResolveBlock = (Any?) -> Void
27
+ public typealias ForgeRejectBlock = (String, String) -> Void
28
+
29
+ @objc(ForgeModuleImpl)
30
+ public final class ForgeModuleImpl: NSObject {
31
+
32
+ @objc public static let shared = ForgeModuleImpl()
33
+
34
+ /// Set by the TurboModule shell; receives the channel body for every kernel event.
35
+ @objc public var eventSink: (([String: Any]) -> Void)?
36
+
37
+ private let lock = NSLock()
38
+ private var instances: [String: NPAWForge] = [:]
39
+ private var subscriptions: [String: ForgeEventSubscription] = [:]
40
+ private var adReporters: [Int: ManualAdReporter] = [:]
41
+ private var nextInstanceId = 1
42
+ private var nextReporterId = 1
43
+ private var listenerCount = 0
44
+
45
+ private static let errUnknownInstance = "E_FORGE_UNKNOWN_INSTANCE"
46
+ private static let errUnknownReporter = "E_FORGE_UNKNOWN_REPORTER"
47
+ private static let errBadArgument = "E_FORGE_BAD_ARGUMENT"
48
+
49
+ private override init() {
50
+ super.init()
51
+ }
52
+
53
+ // MARK: - Registry seam for the video package
54
+
55
+ public func resolveInstance(_ instanceId: String) -> NPAWForge? {
56
+ lock.lock()
57
+ defer { lock.unlock() }
58
+ return instances[instanceId]
59
+ }
60
+
61
+ // MARK: - Lifecycle
62
+
63
+ @objc public func create(
64
+ _ accountCode: String,
65
+ config: [String: Any]?,
66
+ resolve: @escaping ForgeResolveBlock,
67
+ reject: @escaping ForgeRejectBlock
68
+ ) {
69
+ guard !accountCode.trimmingCharacters(in: .whitespaces).isEmpty else {
70
+ reject(Self.errBadArgument, "accountCode must not be blank")
71
+ return
72
+ }
73
+ let builder = NPAWForge.Builder(accountCode: accountCode)
74
+ applyConfig(builder, config: config)
75
+ let forge = builder.build()
76
+
77
+ lock.lock()
78
+ let instanceId = "forge-\(nextInstanceId)"
79
+ nextInstanceId += 1
80
+ instances[instanceId] = forge
81
+ let shouldSubscribe = listenerCount > 0
82
+ lock.unlock()
83
+
84
+ if shouldSubscribe {
85
+ subscribeInstanceEvents(instanceId, forge: forge)
86
+ }
87
+ resolve(instanceId)
88
+ }
89
+
90
+ @objc public func destroy(
91
+ _ instanceId: String,
92
+ resolve: @escaping ForgeResolveBlock,
93
+ reject: @escaping ForgeRejectBlock
94
+ ) {
95
+ lock.lock()
96
+ let forge = instances.removeValue(forKey: instanceId)
97
+ let subscription = subscriptions.removeValue(forKey: instanceId)
98
+ lock.unlock()
99
+
100
+ guard let forge else {
101
+ reject(Self.errUnknownInstance, "No NPAWForge instance '\(instanceId)'")
102
+ return
103
+ }
104
+ subscription?.cancel()
105
+ resolve(Self.outcomeToDictionary(forge.destroy()))
106
+ }
107
+
108
+ @objc public func setLogLevel(
109
+ _ instanceId: String,
110
+ level: String,
111
+ resolve: @escaping ForgeResolveBlock,
112
+ reject: @escaping ForgeRejectBlock
113
+ ) {
114
+ withInstance(instanceId, resolve: resolve, reject: reject) { $0.setLogLevel(level) }
115
+ }
116
+
117
+ // MARK: - App session
118
+
119
+ @objc public func startSession(
120
+ _ instanceId: String,
121
+ metadata: [String: Any]?,
122
+ resolve: @escaping ForgeResolveBlock,
123
+ reject: @escaping ForgeRejectBlock
124
+ ) {
125
+ withInstance(instanceId, resolve: resolve, reject: reject) { $0.startSession(metadata ?? [:]) }
126
+ }
127
+
128
+ @objc public func endSession(
129
+ _ instanceId: String,
130
+ metadata: [String: Any]?,
131
+ resolve: @escaping ForgeResolveBlock,
132
+ reject: @escaping ForgeRejectBlock
133
+ ) {
134
+ withInstance(instanceId, resolve: resolve, reject: reject) { $0.endSession(metadata ?? [:]) }
135
+ }
136
+
137
+ @objc public func trackSessionNavigation(
138
+ _ instanceId: String,
139
+ metadata: [String: Any]?,
140
+ resolve: @escaping ForgeResolveBlock,
141
+ reject: @escaping ForgeRejectBlock
142
+ ) {
143
+ withInstance(instanceId, resolve: resolve, reject: reject) {
144
+ $0.trackSessionNavigation(metadata ?? [:])
145
+ }
146
+ }
147
+
148
+ @objc public func trackSessionEvent(
149
+ _ instanceId: String,
150
+ name: String,
151
+ metadata: [String: Any]?,
152
+ resolve: @escaping ForgeResolveBlock,
153
+ reject: @escaping ForgeRejectBlock
154
+ ) {
155
+ withInstance(instanceId, resolve: resolve, reject: reject) {
156
+ $0.trackSessionEvent(name: name, metadata: metadata ?? [:])
157
+ }
158
+ }
159
+
160
+ @objc public func trackSessionEventStop(
161
+ _ instanceId: String,
162
+ name: String,
163
+ metadata: [String: Any]?,
164
+ resolve: @escaping ForgeResolveBlock,
165
+ reject: @escaping ForgeRejectBlock
166
+ ) {
167
+ withInstance(instanceId, resolve: resolve, reject: reject) {
168
+ $0.trackSessionEventStop(name: name, metadata: metadata ?? [:])
169
+ }
170
+ }
171
+
172
+ @objc public func trackSessionError(
173
+ _ instanceId: String,
174
+ errorCode: String,
175
+ errorMessage: String,
176
+ metadata: [String: Any]?,
177
+ resolve: @escaping ForgeResolveBlock,
178
+ reject: @escaping ForgeRejectBlock
179
+ ) {
180
+ withInstance(instanceId, resolve: resolve, reject: reject) {
181
+ $0.trackSessionError(errorCode: errorCode, errorMessage: errorMessage, metadata: metadata ?? [:])
182
+ }
183
+ }
184
+
185
+ // MARK: - Identity
186
+
187
+ @objc public func loginSuccess(
188
+ _ instanceId: String,
189
+ userId: String,
190
+ options: [String: Any]?,
191
+ resolve: @escaping ForgeResolveBlock,
192
+ reject: @escaping ForgeRejectBlock
193
+ ) {
194
+ withInstance(instanceId, resolve: resolve, reject: reject) {
195
+ $0.loginSuccess(userId: userId, options: Self.stringMap(options))
196
+ }
197
+ }
198
+
199
+ @objc public func loginFailed(
200
+ _ instanceId: String,
201
+ options: [String: Any]?,
202
+ resolve: @escaping ForgeResolveBlock,
203
+ reject: @escaping ForgeRejectBlock
204
+ ) {
205
+ withInstance(instanceId, resolve: resolve, reject: reject) {
206
+ $0.loginFailed(options: Self.stringMap(options))
207
+ }
208
+ }
209
+
210
+ @objc public func logout(
211
+ _ instanceId: String,
212
+ options: [String: Any]?,
213
+ resolve: @escaping ForgeResolveBlock,
214
+ reject: @escaping ForgeRejectBlock
215
+ ) {
216
+ withInstance(instanceId, resolve: resolve, reject: reject) {
217
+ $0.logout(options: Self.stringMap(options))
218
+ }
219
+ }
220
+
221
+ @objc public func profileCreated(
222
+ _ instanceId: String,
223
+ profileId: String,
224
+ options: [String: Any]?,
225
+ resolve: @escaping ForgeResolveBlock,
226
+ reject: @escaping ForgeRejectBlock
227
+ ) {
228
+ withInstance(instanceId, resolve: resolve, reject: reject) {
229
+ $0.profileCreated(profileId: profileId, options: Self.stringMap(options))
230
+ }
231
+ }
232
+
233
+ @objc public func profileSelected(
234
+ _ instanceId: String,
235
+ profileId: String,
236
+ options: [String: Any]?,
237
+ resolve: @escaping ForgeResolveBlock,
238
+ reject: @escaping ForgeRejectBlock
239
+ ) {
240
+ withInstance(instanceId, resolve: resolve, reject: reject) {
241
+ $0.profileSelected(profileId: profileId, options: Self.stringMap(options))
242
+ }
243
+ }
244
+
245
+ @objc public func profileDeleted(
246
+ _ instanceId: String,
247
+ profileId: String,
248
+ resolve: @escaping ForgeResolveBlock,
249
+ reject: @escaping ForgeRejectBlock
250
+ ) {
251
+ withInstance(instanceId, resolve: resolve, reject: reject) {
252
+ $0.profileDeleted(profileId: profileId)
253
+ }
254
+ }
255
+
256
+ // MARK: - Raw / business
257
+
258
+ @objc public func trackRawEvent(
259
+ _ instanceId: String,
260
+ name: String,
261
+ params: [String: Any]?,
262
+ resolve: @escaping ForgeResolveBlock,
263
+ reject: @escaping ForgeRejectBlock
264
+ ) {
265
+ withInstance(instanceId, resolve: resolve, reject: reject) {
266
+ $0.trackRawEvent(name: name, params: params ?? [:])
267
+ }
268
+ }
269
+
270
+ @objc public func trackSearch(
271
+ _ instanceId: String,
272
+ query: String,
273
+ resultCount: Double,
274
+ resolve: @escaping ForgeResolveBlock,
275
+ reject: @escaping ForgeRejectBlock
276
+ ) {
277
+ withInstance(instanceId, resolve: resolve, reject: reject) {
278
+ $0.trackSearch(query: query, resultCount: resultCount < 0 ? nil : Int(resultCount))
279
+ }
280
+ }
281
+
282
+ @objc public func trackSearchResultClick(
283
+ _ instanceId: String,
284
+ query: String,
285
+ options: [String: Any]?,
286
+ resolve: @escaping ForgeResolveBlock,
287
+ reject: @escaping ForgeRejectBlock
288
+ ) {
289
+ let map = Self.stringMap(options)
290
+ let typed = ForgeSearchResultClickOptions(
291
+ contentId: map["contentId"],
292
+ position: map["position"].flatMap { Int($0) }
293
+ )
294
+ withInstance(instanceId, resolve: resolve, reject: reject) {
295
+ $0.trackSearchResultClick(query: query, options: typed)
296
+ }
297
+ }
298
+
299
+ @objc public func trackBusinessEvent(
300
+ _ instanceId: String,
301
+ name: String,
302
+ attributes: [String: Any]?,
303
+ resolve: @escaping ForgeResolveBlock,
304
+ reject: @escaping ForgeRejectBlock
305
+ ) {
306
+ withInstance(instanceId, resolve: resolve, reject: reject) {
307
+ $0.trackBusinessEvent(name: name, attributes: attributes ?? [:])
308
+ }
309
+ }
310
+
311
+ @objc public func trackAttribution(
312
+ _ instanceId: String,
313
+ options: [String: Any]?,
314
+ resolve: @escaping ForgeResolveBlock,
315
+ reject: @escaping ForgeRejectBlock
316
+ ) {
317
+ let map = Self.stringMap(options)
318
+ let typed = ForgeAttributionOptions(
319
+ utmSource: map["utmSource"],
320
+ utmMedium: map["utmMedium"],
321
+ utmCampaign: map["utmCampaign"],
322
+ utmTerm: map["utmTerm"],
323
+ utmContent: map["utmContent"],
324
+ utmUrl: map["utmUrl"]
325
+ )
326
+ withInstance(instanceId, resolve: resolve, reject: reject) {
327
+ $0.trackAttribution(options: typed)
328
+ }
329
+ }
330
+
331
+ // MARK: - Network
332
+
333
+ @objc public func trackNetworkRequest(
334
+ _ instanceId: String,
335
+ record: [String: Any],
336
+ resolve: @escaping ForgeResolveBlock,
337
+ reject: @escaping ForgeRejectBlock
338
+ ) {
339
+ withInstance(instanceId, resolve: resolve, reject: reject) {
340
+ $0.trackNetworkRequest(Self.networkRecord(from: record))
341
+ }
342
+ }
343
+
344
+ @objc public func installNetworkCapture(
345
+ _ instanceId: String,
346
+ options: [String: Any]?,
347
+ resolve: @escaping ForgeResolveBlock,
348
+ reject: @escaping ForgeRejectBlock
349
+ ) {
350
+ lock.lock()
351
+ let forge = instances[instanceId]
352
+ lock.unlock()
353
+ guard let forge else {
354
+ reject(Self.errUnknownInstance, "No NPAWForge instance '\(instanceId)'")
355
+ return
356
+ }
357
+ // URLSession swizzling captures React Native's networking directly.
358
+ // FORGE_RN_HYDRATED marks the staged self-contained pod, where the
359
+ // NetworkCapture sources compile into this module (no import needed).
360
+ #if canImport(NPAWForgeNetworkCapture) || FORGE_RN_HYDRATED
361
+ let outcome = ForgeNetworkCapture.install(forge: forge)
362
+ resolve(Self.outcomeToDictionary(outcome))
363
+ #else
364
+ _ = forge
365
+ resolve(
366
+ Self.outcomeToDictionary(
367
+ ForgeOutcome(
368
+ code: .unsupportedCapability,
369
+ reason: "NPAWForgeNetworkCapture module not present in this build"
370
+ )
371
+ )
372
+ )
373
+ #endif
374
+ }
375
+
376
+ // MARK: - Diagnostics
377
+
378
+ @objc public func snapshotEvents(
379
+ _ instanceId: String,
380
+ resolve: @escaping ForgeResolveBlock,
381
+ reject: @escaping ForgeRejectBlock
382
+ ) {
383
+ lock.lock()
384
+ let forge = instances[instanceId]
385
+ lock.unlock()
386
+ guard let forge else {
387
+ reject(Self.errUnknownInstance, "No NPAWForge instance '\(instanceId)'")
388
+ return
389
+ }
390
+ let items = forge.snapshotEvents().map {
391
+ Self.recordedEventToDictionary(instanceId: instanceId, event: $0)
392
+ }
393
+ resolve(["items": items])
394
+ }
395
+
396
+ // MARK: - Video
397
+
398
+ @objc public func createVideo(
399
+ _ instanceId: String,
400
+ identifier: String,
401
+ options: [String: Any]?,
402
+ resolve: @escaping ForgeResolveBlock,
403
+ reject: @escaping ForgeRejectBlock
404
+ ) {
405
+ lock.lock()
406
+ let forge = instances[instanceId]
407
+ lock.unlock()
408
+ guard let forge else {
409
+ reject(Self.errUnknownInstance, "No NPAWForge instance '\(instanceId)'")
410
+ return
411
+ }
412
+ let videoOptions = VideoOptions.Builder().metadata(Self.stringMap(options)).build()
413
+ _ = forge.videoBuilder().setIdentifier(identifier).setOptions(videoOptions).build()
414
+ resolve(Self.outcomeToDictionary(ForgeOutcome.accepted))
415
+ }
416
+
417
+ @objc public func videoDestroy(
418
+ _ instanceId: String,
419
+ identifier: String,
420
+ resolve: @escaping ForgeResolveBlock,
421
+ reject: @escaping ForgeRejectBlock
422
+ ) {
423
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) { $0.destroy() }
424
+ }
425
+
426
+ @objc public func videoFireInit(
427
+ _ instanceId: String,
428
+ identifier: String,
429
+ params: [String: Any]?,
430
+ resolve: @escaping ForgeResolveBlock,
431
+ reject: @escaping ForgeRejectBlock
432
+ ) {
433
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
434
+ $0.fireInit(params: Self.stringMap(params))
435
+ }
436
+ }
437
+
438
+ @objc public func videoFireStart(
439
+ _ instanceId: String,
440
+ identifier: String,
441
+ params: [String: Any]?,
442
+ forceInit: Bool,
443
+ resolve: @escaping ForgeResolveBlock,
444
+ reject: @escaping ForgeRejectBlock
445
+ ) {
446
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
447
+ $0.fireStart(forceInit: forceInit, params: Self.stringMap(params))
448
+ }
449
+ }
450
+
451
+ @objc public func videoFireJoin(
452
+ _ instanceId: String,
453
+ identifier: String,
454
+ params: [String: Any]?,
455
+ resolve: @escaping ForgeResolveBlock,
456
+ reject: @escaping ForgeRejectBlock
457
+ ) {
458
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
459
+ $0.fireJoin(params: Self.stringMap(params))
460
+ }
461
+ }
462
+
463
+ @objc public func videoFirePause(
464
+ _ instanceId: String,
465
+ identifier: String,
466
+ params: [String: Any]?,
467
+ resolve: @escaping ForgeResolveBlock,
468
+ reject: @escaping ForgeRejectBlock
469
+ ) {
470
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
471
+ $0.firePause(params: Self.stringMap(params))
472
+ }
473
+ }
474
+
475
+ @objc public func videoFireResume(
476
+ _ instanceId: String,
477
+ identifier: String,
478
+ params: [String: Any]?,
479
+ resolve: @escaping ForgeResolveBlock,
480
+ reject: @escaping ForgeRejectBlock
481
+ ) {
482
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
483
+ $0.fireResume(params: Self.stringMap(params))
484
+ }
485
+ }
486
+
487
+ @objc public func videoFireSeekBegin(
488
+ _ instanceId: String,
489
+ identifier: String,
490
+ params: [String: Any]?,
491
+ convertFromBuffer: Bool,
492
+ resolve: @escaping ForgeResolveBlock,
493
+ reject: @escaping ForgeRejectBlock
494
+ ) {
495
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
496
+ $0.fireSeekBegin(convertFromBuffer: convertFromBuffer, params: Self.stringMap(params))
497
+ }
498
+ }
499
+
500
+ @objc public func videoFireSeekEnd(
501
+ _ instanceId: String,
502
+ identifier: String,
503
+ params: [String: Any]?,
504
+ resolve: @escaping ForgeResolveBlock,
505
+ reject: @escaping ForgeRejectBlock
506
+ ) {
507
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
508
+ $0.fireSeekEnd(params: Self.stringMap(params))
509
+ }
510
+ }
511
+
512
+ @objc public func videoFireBufferBegin(
513
+ _ instanceId: String,
514
+ identifier: String,
515
+ params: [String: Any]?,
516
+ convertFromSeek: Bool,
517
+ resolve: @escaping ForgeResolveBlock,
518
+ reject: @escaping ForgeRejectBlock
519
+ ) {
520
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
521
+ $0.fireBufferBegin(convertFromSeek: convertFromSeek, params: Self.stringMap(params))
522
+ }
523
+ }
524
+
525
+ @objc public func videoFireBufferEnd(
526
+ _ instanceId: String,
527
+ identifier: String,
528
+ params: [String: Any]?,
529
+ resolve: @escaping ForgeResolveBlock,
530
+ reject: @escaping ForgeRejectBlock
531
+ ) {
532
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
533
+ $0.fireBufferEnd(params: Self.stringMap(params))
534
+ }
535
+ }
536
+
537
+ @objc public func videoFireStop(
538
+ _ instanceId: String,
539
+ identifier: String,
540
+ params: [String: Any]?,
541
+ resolve: @escaping ForgeResolveBlock,
542
+ reject: @escaping ForgeRejectBlock
543
+ ) {
544
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
545
+ $0.fireStop(params: Self.stringMap(params))
546
+ }
547
+ }
548
+
549
+ @objc public func videoFireError(
550
+ _ instanceId: String,
551
+ identifier: String,
552
+ errorCode: String,
553
+ errorMessage: String,
554
+ errorMetadata: String,
555
+ resolve: @escaping ForgeResolveBlock,
556
+ reject: @escaping ForgeRejectBlock
557
+ ) {
558
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
559
+ $0.fireError(
560
+ errorCode: Self.emptyToNil(errorCode),
561
+ errorMessage: Self.emptyToNil(errorMessage),
562
+ errorMetadata: Self.emptyToNil(errorMetadata)
563
+ )
564
+ }
565
+ }
566
+
567
+ @objc public func videoFireFatalError(
568
+ _ instanceId: String,
569
+ identifier: String,
570
+ errorCode: String,
571
+ errorMessage: String,
572
+ errorMetadata: String,
573
+ resolve: @escaping ForgeResolveBlock,
574
+ reject: @escaping ForgeRejectBlock
575
+ ) {
576
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
577
+ $0.fireFatalError(
578
+ errorCode: Self.emptyToNil(errorCode),
579
+ errorMessage: Self.emptyToNil(errorMessage),
580
+ errorMetadata: Self.emptyToNil(errorMetadata)
581
+ )
582
+ }
583
+ }
584
+
585
+ @objc public func videoFireDestroy(
586
+ _ instanceId: String,
587
+ identifier: String,
588
+ resolve: @escaping ForgeResolveBlock,
589
+ reject: @escaping ForgeRejectBlock
590
+ ) {
591
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
592
+ $0.fireDestroy()
593
+ }
594
+ }
595
+
596
+ @objc public func videoFireEvent(
597
+ _ instanceId: String,
598
+ identifier: String,
599
+ eventName: String,
600
+ dimensions: [String: Any]?,
601
+ values: [String: Any]?,
602
+ topLevelDimensions: [String: Any]?,
603
+ hasEndDatetime: Bool,
604
+ resolve: @escaping ForgeResolveBlock,
605
+ reject: @escaping ForgeRejectBlock
606
+ ) {
607
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
608
+ $0.fireEvent(
609
+ eventName: eventName,
610
+ dimensions: Self.stringMap(dimensions),
611
+ values: Self.doubleMap(values),
612
+ topLevelDimensions: Self.stringMap(topLevelDimensions),
613
+ hasEndDatetime: hasEndDatetime
614
+ )
615
+ }
616
+ }
617
+
618
+ @objc public func videoFireEventEnd(
619
+ _ instanceId: String,
620
+ identifier: String,
621
+ eventName: String,
622
+ dimensions: [String: Any]?,
623
+ values: [String: Any]?,
624
+ topLevelDimensions: [String: Any]?,
625
+ resolve: @escaping ForgeResolveBlock,
626
+ reject: @escaping ForgeRejectBlock
627
+ ) {
628
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
629
+ $0.fireEventEnd(
630
+ eventName: eventName,
631
+ dimensions: Self.stringMap(dimensions),
632
+ values: Self.doubleMap(values),
633
+ topLevelDimensions: Self.stringMap(topLevelDimensions)
634
+ )
635
+ }
636
+ }
637
+
638
+ @objc public func videoFireMetadataUpdate(
639
+ _ instanceId: String,
640
+ identifier: String,
641
+ resolve: @escaping ForgeResolveBlock,
642
+ reject: @escaping ForgeRejectBlock
643
+ ) {
644
+ withVideo(instanceId, identifier: identifier, resolve: resolve, reject: reject) {
645
+ $0.fireMetadataUpdate()
646
+ }
647
+ }
648
+
649
+ // MARK: - Ads
650
+
651
+ @objc public func createAdReporter(
652
+ _ instanceId: String,
653
+ identifier: String,
654
+ adapterVersion: String,
655
+ resolve: @escaping ForgeResolveBlock,
656
+ reject: @escaping ForgeRejectBlock
657
+ ) {
658
+ lock.lock()
659
+ let forge = instances[instanceId]
660
+ lock.unlock()
661
+ guard let forge else {
662
+ reject(Self.errUnknownInstance, "No NPAWForge instance '\(instanceId)'")
663
+ return
664
+ }
665
+ guard let video = forge.getVideoAdapter(identifier: identifier) else {
666
+ reject(Self.errBadArgument, "No video '\(identifier)' on instance '\(instanceId)'")
667
+ return
668
+ }
669
+ let reporter = video.createAdReporter(adapterVersion: Self.emptyToNil(adapterVersion))
670
+ lock.lock()
671
+ let reporterId = nextReporterId
672
+ nextReporterId += 1
673
+ adReporters[reporterId] = reporter
674
+ lock.unlock()
675
+ resolve(reporterId)
676
+ }
677
+
678
+ @objc public func releaseAdReporter(
679
+ _ reporterId: Double,
680
+ resolve: @escaping ForgeResolveBlock,
681
+ reject: @escaping ForgeRejectBlock
682
+ ) {
683
+ lock.lock()
684
+ let removed = adReporters.removeValue(forKey: Int(reporterId))
685
+ lock.unlock()
686
+ guard removed != nil else {
687
+ reject(Self.errUnknownReporter, "No ad reporter \(Int(reporterId))")
688
+ return
689
+ }
690
+ resolve(Self.outcomeToDictionary(ForgeOutcome.accepted))
691
+ }
692
+
693
+ @objc public func adFireManifest(
694
+ _ reporterId: Double,
695
+ params: [String: Any]?,
696
+ resolve: @escaping ForgeResolveBlock,
697
+ reject: @escaping ForgeRejectBlock
698
+ ) {
699
+ withReporter(reporterId, resolve: resolve, reject: reject) {
700
+ $0.fireManifest(params: Self.stringMap(params))
701
+ }
702
+ }
703
+
704
+ @objc public func adFireBreakStart(
705
+ _ reporterId: Double,
706
+ params: [String: Any]?,
707
+ resolve: @escaping ForgeResolveBlock,
708
+ reject: @escaping ForgeRejectBlock
709
+ ) {
710
+ withReporter(reporterId, resolve: resolve, reject: reject) {
711
+ $0.fireBreakStart(params: Self.stringMap(params))
712
+ }
713
+ }
714
+
715
+ @objc public func adFireAdInit(
716
+ _ reporterId: Double,
717
+ params: [String: Any]?,
718
+ resolve: @escaping ForgeResolveBlock,
719
+ reject: @escaping ForgeRejectBlock
720
+ ) {
721
+ withReporter(reporterId, resolve: resolve, reject: reject) {
722
+ $0.fireAdInit(params: Self.stringMap(params))
723
+ }
724
+ }
725
+
726
+ @objc public func adFireStart(
727
+ _ reporterId: Double,
728
+ params: [String: Any]?,
729
+ resolve: @escaping ForgeResolveBlock,
730
+ reject: @escaping ForgeRejectBlock
731
+ ) {
732
+ withReporter(reporterId, resolve: resolve, reject: reject) {
733
+ $0.fireStart(params: Self.stringMap(params))
734
+ }
735
+ }
736
+
737
+ @objc public func adFireJoin(
738
+ _ reporterId: Double,
739
+ params: [String: Any]?,
740
+ resolve: @escaping ForgeResolveBlock,
741
+ reject: @escaping ForgeRejectBlock
742
+ ) {
743
+ withReporter(reporterId, resolve: resolve, reject: reject) {
744
+ $0.fireJoin(params: Self.stringMap(params))
745
+ }
746
+ }
747
+
748
+ @objc public func adFirePause(
749
+ _ reporterId: Double,
750
+ params: [String: Any]?,
751
+ resolve: @escaping ForgeResolveBlock,
752
+ reject: @escaping ForgeRejectBlock
753
+ ) {
754
+ withReporter(reporterId, resolve: resolve, reject: reject) {
755
+ $0.firePause(params: Self.stringMap(params))
756
+ }
757
+ }
758
+
759
+ @objc public func adFireResume(
760
+ _ reporterId: Double,
761
+ params: [String: Any]?,
762
+ resolve: @escaping ForgeResolveBlock,
763
+ reject: @escaping ForgeRejectBlock
764
+ ) {
765
+ withReporter(reporterId, resolve: resolve, reject: reject) {
766
+ $0.fireResume(params: Self.stringMap(params))
767
+ }
768
+ }
769
+
770
+ @objc public func adFireBufferBegin(
771
+ _ reporterId: Double,
772
+ params: [String: Any]?,
773
+ resolve: @escaping ForgeResolveBlock,
774
+ reject: @escaping ForgeRejectBlock
775
+ ) {
776
+ withReporter(reporterId, resolve: resolve, reject: reject) {
777
+ $0.fireBufferBegin(params: Self.stringMap(params))
778
+ }
779
+ }
780
+
781
+ @objc public func adFireBufferEnd(
782
+ _ reporterId: Double,
783
+ params: [String: Any]?,
784
+ resolve: @escaping ForgeResolveBlock,
785
+ reject: @escaping ForgeRejectBlock
786
+ ) {
787
+ withReporter(reporterId, resolve: resolve, reject: reject) {
788
+ $0.fireBufferEnd(params: Self.stringMap(params))
789
+ }
790
+ }
791
+
792
+ @objc public func adFireQuartile(
793
+ _ reporterId: Double,
794
+ quartile: Double,
795
+ resolve: @escaping ForgeResolveBlock,
796
+ reject: @escaping ForgeRejectBlock
797
+ ) {
798
+ withReporter(reporterId, resolve: resolve, reject: reject) {
799
+ $0.fireQuartile(quartile < 0 ? nil : Int(quartile))
800
+ }
801
+ }
802
+
803
+ @objc public func adFireClick(
804
+ _ reporterId: Double,
805
+ params: [String: Any]?,
806
+ resolve: @escaping ForgeResolveBlock,
807
+ reject: @escaping ForgeRejectBlock
808
+ ) {
809
+ withReporter(reporterId, resolve: resolve, reject: reject) {
810
+ $0.fireClick(params: Self.stringMap(params))
811
+ }
812
+ }
813
+
814
+ @objc public func adFireSkip(
815
+ _ reporterId: Double,
816
+ resolve: @escaping ForgeResolveBlock,
817
+ reject: @escaping ForgeRejectBlock
818
+ ) {
819
+ withReporter(reporterId, resolve: resolve, reject: reject) { $0.fireSkip() }
820
+ }
821
+
822
+ @objc public func adFireStop(
823
+ _ reporterId: Double,
824
+ params: [String: Any]?,
825
+ resolve: @escaping ForgeResolveBlock,
826
+ reject: @escaping ForgeRejectBlock
827
+ ) {
828
+ withReporter(reporterId, resolve: resolve, reject: reject) {
829
+ $0.fireStop(params: Self.stringMap(params))
830
+ }
831
+ }
832
+
833
+ @objc public func adFireError(
834
+ _ reporterId: Double,
835
+ errorCode: String,
836
+ errorMessage: String,
837
+ errorMetadata: String,
838
+ resolve: @escaping ForgeResolveBlock,
839
+ reject: @escaping ForgeRejectBlock
840
+ ) {
841
+ withReporter(reporterId, resolve: resolve, reject: reject) {
842
+ $0.fireError(
843
+ errorCode: Self.emptyToNil(errorCode),
844
+ errorMessage: Self.emptyToNil(errorMessage),
845
+ errorMetadata: Self.emptyToNil(errorMetadata)
846
+ )
847
+ }
848
+ }
849
+
850
+ @objc public func adFireFatalError(
851
+ _ reporterId: Double,
852
+ errorCode: String,
853
+ errorMessage: String,
854
+ errorMetadata: String,
855
+ resolve: @escaping ForgeResolveBlock,
856
+ reject: @escaping ForgeRejectBlock
857
+ ) {
858
+ withReporter(reporterId, resolve: resolve, reject: reject) {
859
+ $0.fireFatalError(
860
+ errorCode: Self.emptyToNil(errorCode),
861
+ errorMessage: Self.emptyToNil(errorMessage),
862
+ errorMetadata: Self.emptyToNil(errorMetadata)
863
+ )
864
+ }
865
+ }
866
+
867
+ @objc public func adFireBreakStop(
868
+ _ reporterId: Double,
869
+ params: [String: Any]?,
870
+ resolve: @escaping ForgeResolveBlock,
871
+ reject: @escaping ForgeRejectBlock
872
+ ) {
873
+ withReporter(reporterId, resolve: resolve, reject: reject) {
874
+ $0.fireBreakStop(params: Self.stringMap(params))
875
+ }
876
+ }
877
+
878
+ @objc public func adFireDestroy(
879
+ _ reporterId: Double,
880
+ resolve: @escaping ForgeResolveBlock,
881
+ reject: @escaping ForgeRejectBlock
882
+ ) {
883
+ withReporter(reporterId, resolve: resolve, reject: reject) { $0.fireDestroy() }
884
+ }
885
+
886
+ // MARK: - Event feed
887
+
888
+ @objc public func addListener() {
889
+ lock.lock()
890
+ listenerCount += 1
891
+ let shouldSubscribe = listenerCount == 1
892
+ let live = shouldSubscribe ? instances : [:]
893
+ lock.unlock()
894
+
895
+ if shouldSubscribe {
896
+ live.forEach { subscribeInstanceEvents($0.key, forge: $0.value) }
897
+ }
898
+ }
899
+
900
+ @objc public func removeListeners(_ count: Int) {
901
+ lock.lock()
902
+ listenerCount = max(0, listenerCount - count)
903
+ let shouldCancel = listenerCount == 0
904
+ let cancelled = shouldCancel ? Array(subscriptions.values) : []
905
+ if shouldCancel { subscriptions.removeAll() }
906
+ lock.unlock()
907
+
908
+ cancelled.forEach { $0.cancel() }
909
+ }
910
+
911
+ @objc public func invalidate() {
912
+ lock.lock()
913
+ let cancelled = Array(subscriptions.values)
914
+ let live = Array(instances.values)
915
+ subscriptions.removeAll()
916
+ instances.removeAll()
917
+ adReporters.removeAll()
918
+ listenerCount = 0
919
+ lock.unlock()
920
+
921
+ cancelled.forEach { $0.cancel() }
922
+ live.forEach { _ = $0.destroy() }
923
+ }
924
+
925
+ // MARK: - Internals
926
+
927
+ private func withInstance(
928
+ _ instanceId: String,
929
+ resolve: ForgeResolveBlock,
930
+ reject: ForgeRejectBlock,
931
+ block: (NPAWForge) -> ForgeOutcome
932
+ ) {
933
+ lock.lock()
934
+ let forge = instances[instanceId]
935
+ lock.unlock()
936
+ guard let forge else {
937
+ reject(Self.errUnknownInstance, "No NPAWForge instance '\(instanceId)'")
938
+ return
939
+ }
940
+ resolve(Self.outcomeToDictionary(block(forge)))
941
+ }
942
+
943
+ private func withVideo(
944
+ _ instanceId: String,
945
+ identifier: String,
946
+ resolve: ForgeResolveBlock,
947
+ reject: ForgeRejectBlock,
948
+ block: (VideoAdapter) -> ForgeOutcome
949
+ ) {
950
+ lock.lock()
951
+ let forge = instances[instanceId]
952
+ lock.unlock()
953
+ guard let forge else {
954
+ reject(Self.errUnknownInstance, "No NPAWForge instance '\(instanceId)'")
955
+ return
956
+ }
957
+ guard let video = forge.getVideoAdapter(identifier: identifier) else {
958
+ // The video may have been legitimately destroyed; report facade
959
+ // state semantics instead of a programmer error.
960
+ resolve(Self.outcomeToDictionary(ForgeOutcome.ignoredInvalidState))
961
+ return
962
+ }
963
+ resolve(Self.outcomeToDictionary(block(video)))
964
+ }
965
+
966
+ private func withReporter(
967
+ _ reporterId: Double,
968
+ resolve: ForgeResolveBlock,
969
+ reject: ForgeRejectBlock,
970
+ block: (ManualAdReporter) -> ForgeOutcome
971
+ ) {
972
+ lock.lock()
973
+ let reporter = adReporters[Int(reporterId)]
974
+ lock.unlock()
975
+ guard let reporter else {
976
+ reject(Self.errUnknownReporter, "No ad reporter \(Int(reporterId))")
977
+ return
978
+ }
979
+ resolve(Self.outcomeToDictionary(block(reporter)))
980
+ }
981
+
982
+ private func subscribeInstanceEvents(_ instanceId: String, forge: NPAWForge) {
983
+ lock.lock()
984
+ if subscriptions[instanceId] != nil {
985
+ lock.unlock()
986
+ return
987
+ }
988
+ lock.unlock()
989
+
990
+ let subscription = forge.subscribeEvents { [weak self] event, _ in
991
+ guard let self, let sink = self.eventSink else { return }
992
+ sink(Self.recordedEventToDictionary(instanceId: instanceId, event: event))
993
+ }
994
+ lock.lock()
995
+ subscriptions[instanceId] = subscription
996
+ lock.unlock()
997
+ }
998
+
999
+ private func applyConfig(_ builder: NPAWForge.Builder, config: [String: Any]?) {
1000
+ guard let config else { return }
1001
+ for (key, value) in config {
1002
+ switch key {
1003
+ case "appName":
1004
+ builder.setAppName(value as? String)
1005
+ case "appReleaseVersion":
1006
+ builder.setAppReleaseVersion(value as? String)
1007
+ case "buildVariant":
1008
+ builder.setBuildVariant(value as? String)
1009
+ case "lmaEndpoint":
1010
+ builder.setLmaEndpoint(value as? String)
1011
+ case "analyticsUserAgent":
1012
+ builder.setAnalyticsUserAgent(value as? String)
1013
+ case "logLevel":
1014
+ builder.setLogLevel(value)
1015
+ case "startSessionOnInitialize":
1016
+ builder.setStartSessionOnInitialize(value as? Bool ?? false)
1017
+ case "processEventsBeforeSessionStart":
1018
+ builder.setProcessEventsBeforeSessionStart(value as? Bool ?? false)
1019
+ case "preSessionEventQueueLimitEvents":
1020
+ builder.setPreSessionEventQueueLimitEvents((value as? NSNumber)?.intValue ?? 100)
1021
+ case "preSessionEventQueueMaxAgeMs":
1022
+ builder.setPreSessionEventQueueMaxAgeMs((value as? NSNumber)?.int64Value ?? 0)
1023
+ case "allowLocalHttpForTesting":
1024
+ builder.setLocalPlaintextTransportForTesting(value as? Bool ?? false)
1025
+ case "deviceUuid", "autoDeviceIdentityEnabled":
1026
+ // Android-only today: the Swift Builder lacks these setters.
1027
+ NSLog("[NPAWForgeRN] ForgeConfig key '%@' is Android-only; ignored on iOS", key)
1028
+ default:
1029
+ NSLog("[NPAWForgeRN] Unknown ForgeConfig key '%@' ignored (forward compatibility)", key)
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ private static func networkRecord(from map: [String: Any]) -> NetworkRequestRecord {
1035
+ var record = NetworkRequestRecord(
1036
+ targetUrl: map["targetUrl"] as? String ?? "",
1037
+ method: map["method"] as? String ?? "GET",
1038
+ responseStatusCode: (map["responseStatusCode"] as? NSNumber)?.intValue ?? 0,
1039
+ durationMs: int64(map["durationMs"]) ?? 0,
1040
+ outcome: NetworkRequestOutcome(rawValue: map["outcome"] as? String ?? "unknown") ?? .unknown
1041
+ )
1042
+ record.responseStatusText = map["responseStatusText"] as? String
1043
+ record.errorMessage = map["errorMessage"] as? String
1044
+ record.traceparent = map["traceparent"] as? String
1045
+ record.dnsDurationMs = int64(map["dnsDurationMs"])
1046
+ record.connectDurationMs = int64(map["connectDurationMs"])
1047
+ record.sslDurationMs = int64(map["sslDurationMs"])
1048
+ record.requestDurationMs = int64(map["requestDurationMs"])
1049
+ record.timeToFirstByteMs = int64(map["timeToFirstByteMs"])
1050
+ record.responseDownloadDurationMs = int64(map["responseDownloadDurationMs"])
1051
+ record.redirectDurationMs = int64(map["redirectDurationMs"])
1052
+ record.redirectCount = int64(map["redirectCount"])
1053
+ record.networkProtocol = map["protocol"] as? String
1054
+ record.requestBytes = int64(map["requestBytes"])
1055
+ record.responseBytes = int64(map["responseBytes"])
1056
+ return record
1057
+ }
1058
+
1059
+ private static func int64(_ value: Any?) -> Int64? {
1060
+ (value as? NSNumber)?.int64Value
1061
+ }
1062
+
1063
+ private static func emptyToNil(_ value: String) -> String? {
1064
+ value.isEmpty ? nil : value
1065
+ }
1066
+
1067
+ private static func stringMap(_ map: [String: Any]?) -> [String: String] {
1068
+ guard let map else { return [:] }
1069
+ var result: [String: String] = [:]
1070
+ for (key, value) in map {
1071
+ switch value {
1072
+ case let string as String:
1073
+ result[key] = string
1074
+ case let number as NSNumber:
1075
+ if number === kCFBooleanTrue || number === kCFBooleanFalse {
1076
+ result[key] = number.boolValue ? "true" : "false"
1077
+ } else if number.doubleValue == number.doubleValue.rounded()
1078
+ && abs(number.doubleValue) < 9_007_199_254_740_992 {
1079
+ result[key] = String(number.int64Value)
1080
+ } else {
1081
+ result[key] = number.stringValue
1082
+ }
1083
+ default:
1084
+ result[key] = String(describing: value)
1085
+ }
1086
+ }
1087
+ return result
1088
+ }
1089
+
1090
+ private static func doubleMap(_ map: [String: Any]?) -> [String: Double] {
1091
+ guard let map else { return [:] }
1092
+ var result: [String: Double] = [:]
1093
+ for (key, value) in map {
1094
+ if let number = value as? NSNumber {
1095
+ result[key] = number.doubleValue
1096
+ }
1097
+ }
1098
+ return result
1099
+ }
1100
+
1101
+ static func outcomeToDictionary(_ outcome: ForgeOutcome) -> [String: Any] {
1102
+ var result: [String: Any] = [
1103
+ "code": outcome.code.wireName,
1104
+ "isAccepted": outcome.isAccepted,
1105
+ ]
1106
+ if let reason = outcome.reason {
1107
+ result["reason"] = reason
1108
+ }
1109
+ if !outcome.details.isEmpty {
1110
+ result["details"] = outcome.details
1111
+ }
1112
+ return result
1113
+ }
1114
+
1115
+ static func recordedEventToDictionary(
1116
+ instanceId: String,
1117
+ event: ForgeRecordedEvent
1118
+ ) -> [String: Any] {
1119
+ var result: [String: Any] = [
1120
+ "instanceId": instanceId,
1121
+ "eventName": event.eventName,
1122
+ "timestampMs": Double(event.timestampMs),
1123
+ "payload": event.payload,
1124
+ "urgent": event.urgent,
1125
+ ]
1126
+ if let viewKey = event.viewKey {
1127
+ result["viewKey"] = viewKey
1128
+ }
1129
+ return result
1130
+ }
1131
+ }