@npaw/forge-react-native 9.7.5 → 10.0.0
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/android/build.gradle +1 -1
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +3 -0
- package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +10 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +4 -4
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +11 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +11 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +11 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +11 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +201 -12
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +11 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +48 -8
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +22 -2
- package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +34 -2
- package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +42 -7
- package/ios/ForgeApple/Sources/NPAWForgeCore/ObjectiveCWrappers.swift +14 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/VideoAdapter.swift +5 -5
- package/ios/ForgeApple/Sources/NPAWForgeCore/WireValue.swift +207 -0
- package/ios/ForgeModuleImpl.swift +33 -0
- package/ios/NPAWForgeTurboModule.mm +10 -0
- package/package.json +1 -1
- package/src/NPAWForge.ts +22 -0
- package/src/specs/NativeNPAWForge.ts +3 -0
- package/src/types.ts +8 -0
|
@@ -27,6 +27,12 @@ public struct ForgeAppleConfig: Equatable {
|
|
|
27
27
|
public var processEventsBeforeSessionStart: Bool
|
|
28
28
|
public var preSessionEventQueueLimitEvents: Int
|
|
29
29
|
public var preSessionEventQueueMaxAgeMs: Int64
|
|
30
|
+
/// Logout guard window in milliseconds (session-update mode only): after
|
|
31
|
+
/// `logout()` closes the session, the kernel holds the replacement
|
|
32
|
+
/// session back for this long so a follow-up `loginSuccess` attaches its
|
|
33
|
+
/// own session directly; events raised inside the window are buffered
|
|
34
|
+
/// with their original timestamps. `0` disables the guard.
|
|
35
|
+
public var logoutSessionGuardMs: Int64
|
|
30
36
|
public var logLevel: ForgeLogLevel
|
|
31
37
|
public var deviceUuid: String?
|
|
32
38
|
public var deviceInfo: ForgeDeviceInfo?
|
|
@@ -48,6 +54,7 @@ public struct ForgeAppleConfig: Equatable {
|
|
|
48
54
|
processEventsBeforeSessionStart: Bool = false,
|
|
49
55
|
preSessionEventQueueLimitEvents: Int = 100,
|
|
50
56
|
preSessionEventQueueMaxAgeMs: Int64 = 30_000,
|
|
57
|
+
logoutSessionGuardMs: Int64 = 30_000,
|
|
51
58
|
logLevel: ForgeLogLevel = .warning,
|
|
52
59
|
deviceUuid: String? = nil,
|
|
53
60
|
deviceInfo: ForgeDeviceInfo? = nil,
|
|
@@ -65,6 +72,7 @@ public struct ForgeAppleConfig: Equatable {
|
|
|
65
72
|
self.processEventsBeforeSessionStart = processEventsBeforeSessionStart
|
|
66
73
|
self.preSessionEventQueueLimitEvents = preSessionEventQueueLimitEvents
|
|
67
74
|
self.preSessionEventQueueMaxAgeMs = preSessionEventQueueMaxAgeMs
|
|
75
|
+
self.logoutSessionGuardMs = logoutSessionGuardMs
|
|
68
76
|
self.logLevel = logLevel
|
|
69
77
|
self.deviceUuid = deviceUuid
|
|
70
78
|
self.deviceInfo = deviceInfo
|
|
@@ -143,7 +151,10 @@ public final class ForgeApple {
|
|
|
143
151
|
private let lock = NSRecursiveLock()
|
|
144
152
|
private var recordedEvents: [ForgeRecordedEvent] = []
|
|
145
153
|
private var eventObservers: [UUID: (ForgeRecordedEvent, ForgeOutcome) -> Void] = [:]
|
|
146
|
-
|
|
154
|
+
/// Registration-ordered, matching Android's CopyOnWriteArrayList: when two
|
|
155
|
+
/// listeners edit the same param, the later registration wins
|
|
156
|
+
/// deterministically on both platforms.
|
|
157
|
+
private var onSendRequestListeners: [(id: UUID, listener: (String, inout [String: WireValue]) -> Void)] = []
|
|
147
158
|
private var capturePlanObservers: [UUID: (ForgeCapturePlan) -> Void] = [:]
|
|
148
159
|
private var latestCapturePlan: ForgeCapturePlan?
|
|
149
160
|
private var logObservers: [UUID: (ForgeRenderedLog) -> Void] = [:]
|
|
@@ -359,6 +370,35 @@ public final class ForgeApple {
|
|
|
359
370
|
recordIdentityEvent(kind: "profileDeleted", fields: ["profileId": profileId])
|
|
360
371
|
}
|
|
361
372
|
|
|
373
|
+
/// Hot-updates session-level dimensions without a session boundary:
|
|
374
|
+
/// `param1`..`param20` extraparams, `screenName`, or any dimension the
|
|
375
|
+
/// NQS parameter registry marks updatable - new backend dimensions work
|
|
376
|
+
/// without an SDK release. Identity keys are rejected by the kernel (use
|
|
377
|
+
/// the typed identity API); blank values are dropped because NQS never
|
|
378
|
+
/// erases a session value. Requires the account's remote config to
|
|
379
|
+
/// enable session updates.
|
|
380
|
+
public func updateSession(_ metadata: [String: String], timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
381
|
+
lock.lock()
|
|
382
|
+
defer { lock.unlock() }
|
|
383
|
+
guard !destroyed else { return .ignoredInvalidState }
|
|
384
|
+
return runtime.recordSessionUpdate(
|
|
385
|
+
timemarkMs: timestampMs,
|
|
386
|
+
params: PrivacySanitizer.sanitize(metadata),
|
|
387
|
+
metrics: [:]
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/// Reports numeric session metrics (account-registered custom metrics).
|
|
392
|
+
/// Values aggregate server-side per the account's registered operation
|
|
393
|
+
/// and settle on the session close row; re-sending an unchanged value is
|
|
394
|
+
/// a no-op. Same remote-config gating as `updateSession`.
|
|
395
|
+
public func updateSessionMetrics(_ metrics: [String: Double], timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
396
|
+
lock.lock()
|
|
397
|
+
defer { lock.unlock() }
|
|
398
|
+
guard !destroyed else { return .ignoredInvalidState }
|
|
399
|
+
return runtime.recordSessionUpdate(timemarkMs: timestampMs, params: [:], metrics: metrics)
|
|
400
|
+
}
|
|
401
|
+
|
|
362
402
|
/// Forwards a typed identity transition to the runtime. Recognized fields
|
|
363
403
|
/// (`userId`, `profileId`, `profileType`, `method`, `errorCode`, `reason`,
|
|
364
404
|
/// `userType`) are consumed by the kernel; remaining entries ride as extra
|
|
@@ -492,15 +532,15 @@ public final class ForgeApple {
|
|
|
492
532
|
runtime.registerSessionPreSendHook(hook)
|
|
493
533
|
}
|
|
494
534
|
|
|
495
|
-
public func addOnSendRequestListener(_ listener: @escaping (String, inout [String:
|
|
535
|
+
public func addOnSendRequestListener(_ listener: @escaping (String, inout [String: WireValue]) -> Void) -> ForgeEventSubscription {
|
|
496
536
|
let id = UUID()
|
|
497
537
|
lock.lock()
|
|
498
|
-
onSendRequestListeners
|
|
538
|
+
onSendRequestListeners.append((id: id, listener: listener))
|
|
499
539
|
lock.unlock()
|
|
500
540
|
refreshSessionPreSendHook()
|
|
501
541
|
return ForgeEventSubscription { [weak self] in
|
|
502
542
|
self?.lock.lock()
|
|
503
|
-
self?.onSendRequestListeners.
|
|
543
|
+
self?.onSendRequestListeners.removeAll { $0.id == id }
|
|
504
544
|
self?.lock.unlock()
|
|
505
545
|
self?.refreshSessionPreSendHook()
|
|
506
546
|
}
|
|
@@ -672,7 +712,7 @@ public final class ForgeApple {
|
|
|
672
712
|
let hook = SessionPreSendHook { [weak self] serviceName, params in
|
|
673
713
|
guard let self else { return }
|
|
674
714
|
self.lock.lock()
|
|
675
|
-
let listeners =
|
|
715
|
+
let listeners = self.onSendRequestListeners.map(\.listener)
|
|
676
716
|
self.lock.unlock()
|
|
677
717
|
listeners.forEach { $0(serviceName, ¶ms) }
|
|
678
718
|
}
|
|
@@ -707,13 +747,13 @@ public final class ForgeEventSubscription {
|
|
|
707
747
|
}
|
|
708
748
|
|
|
709
749
|
private final class SessionPreSendHook: RuntimeSessionPreSendHook {
|
|
710
|
-
private let callback: (String, inout [String:
|
|
750
|
+
private let callback: (String, inout [String: WireValue]) -> Void
|
|
711
751
|
|
|
712
|
-
init(callback: @escaping (String, inout [String:
|
|
752
|
+
init(callback: @escaping (String, inout [String: WireValue]) -> Void) {
|
|
713
753
|
self.callback = callback
|
|
714
754
|
}
|
|
715
755
|
|
|
716
|
-
func willSendRequest(serviceName: String, params: inout [String:
|
|
756
|
+
func willSendRequest(serviceName: String, params: inout [String: WireValue]) {
|
|
717
757
|
callback(serviceName, ¶ms)
|
|
718
758
|
}
|
|
719
759
|
}
|
|
@@ -48,17 +48,23 @@ public protocol RuntimeContentMetadataProvider: AnyObject {
|
|
|
48
48
|
func metadataSnapshot(timemarkMs: Int64, elapsedRealtimeMs: Int64) -> MetadataSnapshot?
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/// Per-view pre-send hook. `params` is the request payload as the typed
|
|
52
|
+
/// values the wire will carry — see ``WireValue`` for the encoding and the
|
|
53
|
+
/// two deliberate escape hatches (string re-typing, byte-identical
|
|
54
|
+
/// untouched values).
|
|
51
55
|
public protocol RuntimeVideoPreSendHook: AnyObject {
|
|
52
56
|
func willSendRequest(
|
|
53
57
|
eventName: String,
|
|
54
58
|
view: RuntimeVideoView,
|
|
55
59
|
serviceName: String,
|
|
56
|
-
params: inout [String:
|
|
60
|
+
params: inout [String: WireValue]
|
|
57
61
|
)
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
/// Session-scope counterpart of ``RuntimeVideoPreSendHook``; same typed
|
|
65
|
+
/// payload contract.
|
|
60
66
|
public protocol RuntimeSessionPreSendHook: AnyObject {
|
|
61
|
-
func willSendRequest(serviceName: String, params: inout [String:
|
|
67
|
+
func willSendRequest(serviceName: String, params: inout [String: WireValue])
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
public protocol ForgeRuntime: AnyObject {
|
|
@@ -91,6 +97,10 @@ public protocol ForgeRuntime: AnyObject {
|
|
|
91
97
|
/// `profileType`, `method`, `errorCode`, `reason`, `userType`), forwards
|
|
92
98
|
/// the rest as extra session-event params, and emits the taxonomy event.
|
|
93
99
|
func recordIdentityEvent(kind: String, timemarkMs: Int64, fields: [String: String]) -> ForgeOutcome
|
|
100
|
+
/// Hot session update (session-update mode): flat dimension params plus
|
|
101
|
+
/// numeric session metrics staged by the kernel and carried on the next
|
|
102
|
+
/// session event or an update beat.
|
|
103
|
+
func recordSessionUpdate(timemarkMs: Int64, params: [String: String], metrics: [String: Double]) -> ForgeOutcome
|
|
94
104
|
func recordApplicationForeground(_ foreground: Bool, timestampMs: Int64) -> ForgeOutcome
|
|
95
105
|
func recordNetworkEvent(_ record: NetworkRequestRecord) -> ForgeOutcome
|
|
96
106
|
func recordPlayerEvent(
|
|
@@ -138,6 +148,9 @@ public extension ForgeRuntime {
|
|
|
138
148
|
/// Additive default: runtimes without kernel identity support report the
|
|
139
149
|
/// capability gap instead of silently accepting the transition.
|
|
140
150
|
func recordIdentityEvent(kind: String, timemarkMs: Int64, fields: [String: String]) -> ForgeOutcome { .unsupportedCapability }
|
|
151
|
+
/// Additive default: runtimes without kernel session-update support report
|
|
152
|
+
/// the capability gap instead of silently accepting the update.
|
|
153
|
+
func recordSessionUpdate(timemarkMs: Int64, params: [String: String], metrics: [String: Double]) -> ForgeOutcome { .unsupportedCapability }
|
|
141
154
|
/// Additive default: a runtime with no kernel behind it has nowhere to keep
|
|
142
155
|
/// the observed connection class, so it accepts and forgets.
|
|
143
156
|
func setAutoConnectionType(_ connectionType: String?) -> ForgeOutcome { .accepted }
|
|
@@ -171,6 +184,7 @@ public final class UnavailableForgeRuntime: ForgeRuntime {
|
|
|
171
184
|
public func createVideoView(timestampMs: Int64, options: VideoOptions) -> RuntimeVideoView { RuntimeVideoView(id: -1) }
|
|
172
185
|
public func recordEvent(_ event: ForgeRecordedEvent) -> ForgeOutcome { failed() }
|
|
173
186
|
public func recordIdentityEvent(kind: String, timemarkMs: Int64, fields: [String: String]) -> ForgeOutcome { failed() }
|
|
187
|
+
public func recordSessionUpdate(timemarkMs: Int64, params: [String: String], metrics: [String: Double]) -> ForgeOutcome { failed() }
|
|
174
188
|
public func recordApplicationForeground(_ foreground: Bool, timestampMs: Int64) -> ForgeOutcome { failed() }
|
|
175
189
|
public func recordNetworkEvent(_ record: NetworkRequestRecord) -> ForgeOutcome { failed() }
|
|
176
190
|
public func recordPlayerEvent(
|
|
@@ -217,6 +231,7 @@ public final class NativeForgeRuntimeFactory: ForgeRuntimeFactory {
|
|
|
217
231
|
public final class RecordingForgeRuntime: ForgeRuntime {
|
|
218
232
|
public private(set) var events: [ForgeRecordedEvent] = []
|
|
219
233
|
public private(set) var identityEvents: [(kind: String, timemarkMs: Int64, fields: [String: String])] = []
|
|
234
|
+
public private(set) var sessionUpdates: [(timemarkMs: Int64, params: [String: String], metrics: [String: Double])] = []
|
|
220
235
|
public private(set) var playerEvents: [(RuntimeVideoView, PlayerEvent, PlayerMetadataSnapshot)] = []
|
|
221
236
|
public private(set) var playerEventTimestamps: [(timemarkMs: Int64, elapsedRealtimeMs: Int64)] = []
|
|
222
237
|
public private(set) var playerEventOptions: [VideoOptions] = []
|
|
@@ -290,6 +305,11 @@ public final class RecordingForgeRuntime: ForgeRuntime {
|
|
|
290
305
|
identityEvents.append((kind, timemarkMs, fields))
|
|
291
306
|
return .accepted
|
|
292
307
|
}
|
|
308
|
+
|
|
309
|
+
public func recordSessionUpdate(timemarkMs: Int64, params: [String: String], metrics: [String: Double]) -> ForgeOutcome {
|
|
310
|
+
sessionUpdates.append((timemarkMs, params, metrics))
|
|
311
|
+
return .accepted
|
|
312
|
+
}
|
|
293
313
|
public func recordApplicationForeground(_ foreground: Bool, timestampMs: Int64) -> ForgeOutcome {
|
|
294
314
|
events.append(
|
|
295
315
|
ForgeRecordedEvent(
|
|
@@ -40,7 +40,7 @@ public final class NPAWForge: NSObject {
|
|
|
40
40
|
/// `scripts/apple/publish-local-shipped.sh` rewrites this literal to the
|
|
41
41
|
/// release version when staging the shipped package, and fails loudly if
|
|
42
42
|
/// it cannot find it. Keep it a plain stored literal for that reason.
|
|
43
|
-
public static let defaultPluginVersion = "
|
|
43
|
+
public static let defaultPluginVersion = "10.0.0"
|
|
44
44
|
public static let defaultAppName = "apple"
|
|
45
45
|
public static let defaultBuildVariant = "release"
|
|
46
46
|
public static let defaultLmaEndpoint = "https://lma.npaw.com"
|
|
@@ -135,7 +135,7 @@ public final class NPAWForge: NSObject {
|
|
|
135
135
|
kernel.capturePlan()
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
public func addOnSendRequestListener(_ listener: @escaping (String, inout [String:
|
|
138
|
+
public func addOnSendRequestListener(_ listener: @escaping (String, inout [String: WireValue]) -> Void) -> ForgeEventSubscription {
|
|
139
139
|
kernel.addOnSendRequestListener(listener)
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -200,6 +200,25 @@ public final class NPAWForge: NSObject {
|
|
|
200
200
|
kernel.trackAttribution(options: options)
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/// Hot-updates session-level dimensions without a session boundary:
|
|
204
|
+
/// `param1`..`param20` extraparams, `screenName`, or any dimension the
|
|
205
|
+
/// NQS parameter registry marks updatable - new backend dimensions work
|
|
206
|
+
/// without an SDK release. The change reaches NQS on the next session
|
|
207
|
+
/// event within one second, or on an immediate beat after that window.
|
|
208
|
+
/// Identity keys are rejected - use the typed identity API. Requires the
|
|
209
|
+
/// account's remote config to enable session updates.
|
|
210
|
+
@objc public func updateSession(_ metadata: [String: Any]) -> ForgeOutcome {
|
|
211
|
+
kernel.updateSession(metadata.toStringMap())
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Reports numeric session metrics (account-registered custom metrics).
|
|
215
|
+
/// Values aggregate server-side per the account's registered operation
|
|
216
|
+
/// and settle on the session close row; re-sending an unchanged value is
|
|
217
|
+
/// a no-op. Same remote-config gating as `updateSession(_:)`.
|
|
218
|
+
@objc public func updateSessionMetrics(_ metrics: [String: Double]) -> ForgeOutcome {
|
|
219
|
+
kernel.updateSessionMetrics(metrics)
|
|
220
|
+
}
|
|
221
|
+
|
|
203
222
|
/// Reports a successful login; the kernel emits `login
|
|
204
223
|
/// {authStatus:"success"}` and atomically restarts the app session when
|
|
205
224
|
/// the effective identity changed.
|
|
@@ -392,6 +411,7 @@ public final class NPAWForge: NSObject {
|
|
|
392
411
|
private var processEventsBeforeSessionStart = false
|
|
393
412
|
private var preSessionEventQueueLimitEvents = 100
|
|
394
413
|
private var preSessionEventQueueMaxAgeMs: Int64 = 30_000
|
|
414
|
+
private var logoutSessionGuardMs: Int64 = 30_000
|
|
395
415
|
private var logLevel: ForgeLogLevel = .warning
|
|
396
416
|
private var autoDeviceIdentityEnabled = true
|
|
397
417
|
private var autoConnectionTypeEnabled = true
|
|
@@ -464,6 +484,17 @@ public final class NPAWForge: NSObject {
|
|
|
464
484
|
return self
|
|
465
485
|
}
|
|
466
486
|
|
|
487
|
+
/// Logout guard window in milliseconds (session-update mode only):
|
|
488
|
+
/// after `logout()` closes the session, the kernel holds the
|
|
489
|
+
/// replacement session back for this long so a follow-up
|
|
490
|
+
/// `loginSuccess` attaches its own session directly; events raised
|
|
491
|
+
/// inside the window are buffered with their original timestamps.
|
|
492
|
+
/// `0` disables the guard. Default 30 000.
|
|
493
|
+
@discardableResult public func setLogoutSessionGuardMs(_ guardMs: Int64) -> Builder {
|
|
494
|
+
logoutSessionGuardMs = max(0, guardMs)
|
|
495
|
+
return self
|
|
496
|
+
}
|
|
497
|
+
|
|
467
498
|
@discardableResult public func setLogLevel(_ value: Any?) -> Builder {
|
|
468
499
|
logLevel = ForgeLogLevel.from(value)
|
|
469
500
|
return self
|
|
@@ -562,6 +593,7 @@ public final class NPAWForge: NSObject {
|
|
|
562
593
|
processEventsBeforeSessionStart: processEventsBeforeSessionStart,
|
|
563
594
|
preSessionEventQueueLimitEvents: preSessionEventQueueLimitEvents,
|
|
564
595
|
preSessionEventQueueMaxAgeMs: preSessionEventQueueMaxAgeMs,
|
|
596
|
+
logoutSessionGuardMs: logoutSessionGuardMs,
|
|
565
597
|
logLevel: logLevel,
|
|
566
598
|
deviceUuid: resolvedDeviceUuid,
|
|
567
599
|
deviceInfo: resolvedDeviceInfo?.isEmpty == true ? nil : resolvedDeviceInfo,
|
|
@@ -47,7 +47,8 @@ final class UniFFIForgeRuntime: ForgeRuntime {
|
|
|
47
47
|
processEventsBeforeSessionStart: config.processEventsBeforeSessionStart,
|
|
48
48
|
queueLimitEvents: UInt64(max(0, config.preSessionEventQueueLimitEvents)),
|
|
49
49
|
maxAgeMs: u64(config.preSessionEventQueueMaxAgeMs)
|
|
50
|
-
)
|
|
50
|
+
),
|
|
51
|
+
logoutSessionGuardMs: u64(config.logoutSessionGuardMs)
|
|
51
52
|
))
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -132,6 +133,14 @@ final class UniFFIForgeRuntime: ForgeRuntime {
|
|
|
132
133
|
))
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
func recordSessionUpdate(timemarkMs: Int64, params: [String: String], metrics: [String: Double]) -> ForgeOutcome {
|
|
137
|
+
outcome(bridge.recordSessionUpdate(
|
|
138
|
+
timemarkMs: u64(timemarkMs),
|
|
139
|
+
params: params,
|
|
140
|
+
metrics: metrics
|
|
141
|
+
))
|
|
142
|
+
}
|
|
143
|
+
|
|
135
144
|
func recordApplicationForeground(_ foreground: Bool, timestampMs: Int64) -> ForgeOutcome {
|
|
136
145
|
outcome(bridge.recordApplicationForeground(foreground: foreground, timemarkMs: u64(timestampMs)))
|
|
137
146
|
}
|
|
@@ -653,6 +662,32 @@ private final class UniFFIContentMetadataProvider: AppleContentMetadataProvider,
|
|
|
653
662
|
}
|
|
654
663
|
}
|
|
655
664
|
|
|
665
|
+
// MARK: - WireValue boundary conversions
|
|
666
|
+
|
|
667
|
+
extension WireValue {
|
|
668
|
+
init(bridged: AppleWireValue) {
|
|
669
|
+
switch bridged {
|
|
670
|
+
case let .str(value): self = .string(value)
|
|
671
|
+
case let .int(value): self = .int(value)
|
|
672
|
+
case let .num(value): self = .number(value)
|
|
673
|
+
case let .bool(value): self = .bool(value)
|
|
674
|
+
case let .object(value): self = .object(value.mapValues(WireValue.init(bridged:)))
|
|
675
|
+
case let .array(value): self = .array(value.map(WireValue.init(bridged:)))
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
var bridged: AppleWireValue {
|
|
680
|
+
switch self {
|
|
681
|
+
case let .string(value): return .str(value: value)
|
|
682
|
+
case let .int(value): return .int(value: value)
|
|
683
|
+
case let .number(value): return .num(value: value)
|
|
684
|
+
case let .bool(value): return .bool(value: value)
|
|
685
|
+
case let .object(value): return .object(value: value.mapValues(\.bridged))
|
|
686
|
+
case let .array(value): return .array(value: value.map(\.bridged))
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
656
691
|
private final class UniFFIVideoPreSendHook: AppleVideoPreSendHook, @unchecked Sendable {
|
|
657
692
|
private let view: RuntimeVideoView
|
|
658
693
|
private let hook: RuntimeVideoPreSendHook
|
|
@@ -662,15 +697,15 @@ private final class UniFFIVideoPreSendHook: AppleVideoPreSendHook, @unchecked Se
|
|
|
662
697
|
self.hook = hook
|
|
663
698
|
}
|
|
664
699
|
|
|
665
|
-
func willSend(eventName: String, viewId: UInt64, serviceName: String, params: [String:
|
|
666
|
-
var mutableParams = params
|
|
700
|
+
func willSend(eventName: String, viewId: UInt64, serviceName: String, params: [String: AppleWireValue]) -> [String: AppleWireValue] {
|
|
701
|
+
var mutableParams = params.mapValues(WireValue.init(bridged:))
|
|
667
702
|
hook.willSendRequest(
|
|
668
703
|
eventName: eventName,
|
|
669
704
|
view: view.id == i64(viewId) ? view : RuntimeVideoView(id: i64(viewId)),
|
|
670
705
|
serviceName: serviceName,
|
|
671
706
|
params: &mutableParams
|
|
672
707
|
)
|
|
673
|
-
return mutableParams
|
|
708
|
+
return mutableParams.mapValues(\.bridged)
|
|
674
709
|
}
|
|
675
710
|
}
|
|
676
711
|
|
|
@@ -681,10 +716,10 @@ private final class UniFFISessionPreSendHook: AppleSessionPreSendHook, @unchecke
|
|
|
681
716
|
self.hook = hook
|
|
682
717
|
}
|
|
683
718
|
|
|
684
|
-
func willSend(serviceName: String, params: [String:
|
|
685
|
-
var mutableParams = params
|
|
719
|
+
func willSend(serviceName: String, params: [String: AppleWireValue]) -> [String: AppleWireValue] {
|
|
720
|
+
var mutableParams = params.mapValues(WireValue.init(bridged:))
|
|
686
721
|
hook.willSendRequest(serviceName: serviceName, params: &mutableParams)
|
|
687
|
-
return mutableParams
|
|
722
|
+
return mutableParams.mapValues(\.bridged)
|
|
688
723
|
}
|
|
689
724
|
}
|
|
690
725
|
|
|
@@ -134,6 +134,20 @@ public final class NPAWForgeObjC: NSObject {
|
|
|
134
134
|
))
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
@objc(updateSessionWithMetadata:)
|
|
138
|
+
public func updateSession(metadata: NSDictionary?) -> ForgeOutcome {
|
|
139
|
+
swiftForge.updateSession(metadata as? [String: Any] ?? [:])
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@objc(updateSessionMetricsWithMetrics:)
|
|
143
|
+
public func updateSessionMetrics(metrics: NSDictionary?) -> ForgeOutcome {
|
|
144
|
+
var converted: [String: Double] = [:]
|
|
145
|
+
for case let (key as String, value as NSNumber) in metrics ?? [:] {
|
|
146
|
+
converted[key] = value.doubleValue
|
|
147
|
+
}
|
|
148
|
+
return swiftForge.updateSessionMetrics(converted)
|
|
149
|
+
}
|
|
150
|
+
|
|
137
151
|
@objc(loginSuccessWithUserId:options:)
|
|
138
152
|
public func loginSuccess(userId: String, options: NSDictionary?) -> ForgeOutcome {
|
|
139
153
|
swiftForge.loginSuccess(userId: userId, options: options.toStringStringMap())
|
|
@@ -70,7 +70,7 @@ public final class VideoAdapter: RuntimeContentMetadataProvider {
|
|
|
70
70
|
private var effectiveOptions: VideoOptions
|
|
71
71
|
private let runtimeView: RuntimeVideoView
|
|
72
72
|
private var destroyed = false
|
|
73
|
-
private var willSendRequestListener: ((String, String, VideoAdapter, inout [String:
|
|
73
|
+
private var willSendRequestListener: ((String, String, VideoAdapter, inout [String: WireValue]) -> Void)?
|
|
74
74
|
|
|
75
75
|
public private(set) var playerAdapter: PlayerAdapter?
|
|
76
76
|
public private(set) var adAdapter: AdAdapter?
|
|
@@ -113,7 +113,7 @@ public final class VideoAdapter: RuntimeContentMetadataProvider {
|
|
|
113
113
|
/// addressed to `error` and `stop`, both carrying the same `eventName` — so
|
|
114
114
|
/// without the service name that correct pair is indistinguishable from a
|
|
115
115
|
/// duplicate.
|
|
116
|
-
public func setWillSendRequestListener(_ listener: ((String, String, VideoAdapter, inout [String:
|
|
116
|
+
public func setWillSendRequestListener(_ listener: ((String, String, VideoAdapter, inout [String: WireValue]) -> Void)?) -> ForgeOutcome {
|
|
117
117
|
guard !destroyed else { return .ignoredInvalidState }
|
|
118
118
|
willSendRequestListener = listener
|
|
119
119
|
let hook = listener.map { callback in
|
|
@@ -475,13 +475,13 @@ private extension AdInsertionType {
|
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
private final class VideoPreSendHook: RuntimeVideoPreSendHook {
|
|
478
|
-
private let callback: (String, RuntimeVideoView, String, inout [String:
|
|
478
|
+
private let callback: (String, RuntimeVideoView, String, inout [String: WireValue]) -> Void
|
|
479
479
|
|
|
480
|
-
init(callback: @escaping (String, RuntimeVideoView, String, inout [String:
|
|
480
|
+
init(callback: @escaping (String, RuntimeVideoView, String, inout [String: WireValue]) -> Void) {
|
|
481
481
|
self.callback = callback
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
-
func willSendRequest(eventName: String, view: RuntimeVideoView, serviceName: String, params: inout [String:
|
|
484
|
+
func willSendRequest(eventName: String, view: RuntimeVideoView, serviceName: String, params: inout [String: WireValue]) {
|
|
485
485
|
callback(eventName, view, serviceName, ¶ms)
|
|
486
486
|
}
|
|
487
487
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
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: Typed wire value the pre-send request hooks see and edit.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
/// One request-body param as the wire carries it.
|
|
18
|
+
///
|
|
19
|
+
/// The NQS POST body is typed JSON: quantities are numbers, flags are
|
|
20
|
+
/// booleans, and nested documents (`entities`, `metrics`, `dimensions`,
|
|
21
|
+
/// `deviceInfo`, …) are nested objects. Pre-send hooks see and edit exactly
|
|
22
|
+
/// that encoding — what you read is what the wire says, and what you write is
|
|
23
|
+
/// what the wire will say.
|
|
24
|
+
///
|
|
25
|
+
/// Literal conformances keep hook code natural:
|
|
26
|
+
///
|
|
27
|
+
/// ```swift
|
|
28
|
+
/// params["contentId"] = "movie-42" // .string
|
|
29
|
+
/// params["bitrate"] = 4_316_400 // .int
|
|
30
|
+
/// params["playhead"] = 645.75 // .number
|
|
31
|
+
/// params["live"] = true // .bool
|
|
32
|
+
/// params["dimensions"] = ["genre": "sports"]
|
|
33
|
+
/// ```
|
|
34
|
+
///
|
|
35
|
+
/// Two escape hatches are deliberate:
|
|
36
|
+
/// - A string written where a numeric or boolean param is declared (for
|
|
37
|
+
/// example `params["bitrate"] = "4316400"`, the all-strings habit) is
|
|
38
|
+
/// re-typed by the kernel before sending, so ported hook code keeps
|
|
39
|
+
/// producing the same wire.
|
|
40
|
+
/// - A param the hook leaves content-equal is sent byte-identical to its
|
|
41
|
+
/// pre-hook form — key order of untouched nested documents included — so
|
|
42
|
+
/// registering a hook never changes traffic the hook did not edit.
|
|
43
|
+
public enum WireValue: Equatable, Hashable {
|
|
44
|
+
case string(String)
|
|
45
|
+
/// An integral number. Serializes without a decimal point (`28899`).
|
|
46
|
+
case int(Int64)
|
|
47
|
+
/// A floating-point number. Keeps its spelling (`1.0` stays `1.0`).
|
|
48
|
+
case number(Double)
|
|
49
|
+
case bool(Bool)
|
|
50
|
+
case object([String: WireValue])
|
|
51
|
+
case array([WireValue])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
extension WireValue: ExpressibleByStringLiteral {
|
|
55
|
+
public init(stringLiteral value: String) { self = .string(value) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
extension WireValue: ExpressibleByIntegerLiteral {
|
|
59
|
+
public init(integerLiteral value: Int64) { self = .int(value) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
extension WireValue: ExpressibleByFloatLiteral {
|
|
63
|
+
public init(floatLiteral value: Double) { self = .number(value) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
extension WireValue: ExpressibleByBooleanLiteral {
|
|
67
|
+
public init(booleanLiteral value: Bool) { self = .bool(value) }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
extension WireValue: ExpressibleByDictionaryLiteral {
|
|
71
|
+
public init(dictionaryLiteral elements: (String, WireValue)...) {
|
|
72
|
+
self = .object(Dictionary(uniqueKeysWithValues: elements))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
extension WireValue: ExpressibleByArrayLiteral {
|
|
77
|
+
public init(arrayLiteral elements: WireValue...) { self = .array(elements) }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public extension WireValue {
|
|
81
|
+
/// The string payload, when this value is a string.
|
|
82
|
+
var stringValue: String? {
|
|
83
|
+
if case let .string(value) = self { return value }
|
|
84
|
+
return nil
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// The integral payload. An integral `.number` (`4.0`) converts; a
|
|
88
|
+
/// fractional one does not.
|
|
89
|
+
var intValue: Int64? {
|
|
90
|
+
switch self {
|
|
91
|
+
case let .int(value): return value
|
|
92
|
+
case let .number(value)
|
|
93
|
+
where value.truncatingRemainder(dividingBy: 1) == 0
|
|
94
|
+
&& value >= -9_007_199_254_740_992 && value <= 9_007_199_254_740_992:
|
|
95
|
+
return Int64(value)
|
|
96
|
+
default: return nil
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// The numeric payload, integral or floating.
|
|
101
|
+
var doubleValue: Double? {
|
|
102
|
+
switch self {
|
|
103
|
+
case let .int(value): return Double(value)
|
|
104
|
+
case let .number(value): return value
|
|
105
|
+
default: return nil
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// The boolean payload, when this value is a boolean.
|
|
110
|
+
var boolValue: Bool? {
|
|
111
|
+
if case let .bool(value) = self { return value }
|
|
112
|
+
return nil
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// The nested object payload, when this value is an object.
|
|
116
|
+
var objectValue: [String: WireValue]? {
|
|
117
|
+
if case let .object(value) = self { return value }
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// The nested array payload, when this value is an array.
|
|
122
|
+
var arrayValue: [WireValue]? {
|
|
123
|
+
if case let .array(value) = self { return value }
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// The text this value renders to where a string is needed: the payload
|
|
128
|
+
/// itself for a string, the canonical JSON text for everything else.
|
|
129
|
+
///
|
|
130
|
+
/// Scalars render exactly as the wire does. One divergence is inherent to
|
|
131
|
+
/// Swift: `.object` renders with its keys **sorted**, because a Swift
|
|
132
|
+
/// dictionary has no insertion order to preserve — the wire itself keeps
|
|
133
|
+
/// authored order (untouched values byte-repaired by the kernel), so use
|
|
134
|
+
/// this for reading and logging, not as a byte-level mirror of a nested
|
|
135
|
+
/// document on the wire.
|
|
136
|
+
var textValue: String {
|
|
137
|
+
switch self {
|
|
138
|
+
case let .string(value): return value
|
|
139
|
+
case let .int(value): return String(value)
|
|
140
|
+
case let .number(value): return jsonNumberText(value)
|
|
141
|
+
case let .bool(value): return value ? "true" : "false"
|
|
142
|
+
case .object, .array:
|
|
143
|
+
return jsonText
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
extension WireValue: CustomStringConvertible {
|
|
149
|
+
public var description: String { textValue }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public extension WireValue {
|
|
153
|
+
/// Canonical JSON text of this value. Object keys render sorted — see
|
|
154
|
+
/// ``textValue`` for why.
|
|
155
|
+
var jsonText: String {
|
|
156
|
+
switch self {
|
|
157
|
+
case let .string(value):
|
|
158
|
+
return escapedJSONString(value)
|
|
159
|
+
case let .int(value):
|
|
160
|
+
return String(value)
|
|
161
|
+
case let .number(value):
|
|
162
|
+
return jsonNumberText(value)
|
|
163
|
+
case let .bool(value):
|
|
164
|
+
return value ? "true" : "false"
|
|
165
|
+
case let .object(value):
|
|
166
|
+
let fields = value
|
|
167
|
+
.sorted { $0.key < $1.key }
|
|
168
|
+
.map { "\(escapedJSONString($0.key)):\($0.value.jsonText)" }
|
|
169
|
+
return "{\(fields.joined(separator: ","))}"
|
|
170
|
+
case let .array(value):
|
|
171
|
+
return "[\(value.map(\.jsonText).joined(separator: ","))]"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Renders a double the way the wire does: integral values keep one decimal
|
|
177
|
+
/// (`1.0`), fractional values keep their precision.
|
|
178
|
+
private func jsonNumberText(_ value: Double) -> String {
|
|
179
|
+
guard value.isFinite else { return String(describing: value) }
|
|
180
|
+
if value.truncatingRemainder(dividingBy: 1) == 0, abs(value) < 1e15 {
|
|
181
|
+
return String(format: "%.1f", value)
|
|
182
|
+
}
|
|
183
|
+
var text = String(value)
|
|
184
|
+
// Swift renders some doubles in exponent form ("1e+16"); JSON accepts it.
|
|
185
|
+
if text.hasSuffix(".0") { text.removeLast(2); text += ".0" }
|
|
186
|
+
return text
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private func escapedJSONString(_ value: String) -> String {
|
|
190
|
+
var escaped = "\""
|
|
191
|
+
for scalar in value.unicodeScalars {
|
|
192
|
+
switch scalar {
|
|
193
|
+
case "\"": escaped += "\\\""
|
|
194
|
+
case "\\": escaped += "\\\\"
|
|
195
|
+
case "\n": escaped += "\\n"
|
|
196
|
+
case "\r": escaped += "\\r"
|
|
197
|
+
case "\t": escaped += "\\t"
|
|
198
|
+
default:
|
|
199
|
+
if scalar.value < 0x20 {
|
|
200
|
+
escaped += String(format: "\\u%04x", scalar.value)
|
|
201
|
+
} else {
|
|
202
|
+
escaped.unicodeScalars.append(scalar)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return escaped + "\""
|
|
207
|
+
}
|
|
@@ -194,6 +194,37 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// MARK: - Session updates
|
|
198
|
+
|
|
199
|
+
@objc public func updateSession(
|
|
200
|
+
_ instanceId: String,
|
|
201
|
+
metadata: [String: Any]?,
|
|
202
|
+
resolve: @escaping ForgeResolveBlock,
|
|
203
|
+
reject: @escaping ForgeRejectBlock
|
|
204
|
+
) {
|
|
205
|
+
withInstance(instanceId, resolve: resolve, reject: reject) {
|
|
206
|
+
$0.updateSession(metadata ?? [:])
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@objc public func updateSessionMetrics(
|
|
211
|
+
_ instanceId: String,
|
|
212
|
+
metrics: [String: Any]?,
|
|
213
|
+
resolve: @escaping ForgeResolveBlock,
|
|
214
|
+
reject: @escaping ForgeRejectBlock
|
|
215
|
+
) {
|
|
216
|
+
var converted: [String: Double] = [:]
|
|
217
|
+
for (key, value) in metrics ?? [:] {
|
|
218
|
+
if let number = value as? NSNumber {
|
|
219
|
+
converted[key] = number.doubleValue
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
let numeric = converted
|
|
223
|
+
withInstance(instanceId, resolve: resolve, reject: reject) {
|
|
224
|
+
$0.updateSessionMetrics(numeric)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
197
228
|
// MARK: - Identity
|
|
198
229
|
|
|
199
230
|
@objc public func loginSuccess(
|
|
@@ -1045,6 +1076,8 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
1045
1076
|
builder.setPreSessionEventQueueLimitEvents((value as? NSNumber)?.intValue ?? 100)
|
|
1046
1077
|
case "preSessionEventQueueMaxAgeMs":
|
|
1047
1078
|
builder.setPreSessionEventQueueMaxAgeMs((value as? NSNumber)?.int64Value ?? 0)
|
|
1079
|
+
case "logoutSessionGuardMs":
|
|
1080
|
+
builder.setLogoutSessionGuardMs((value as? NSNumber)?.int64Value ?? 30_000)
|
|
1048
1081
|
case "allowLocalHttpForTesting":
|
|
1049
1082
|
builder.setLocalPlaintextTransportForTesting(value as? Bool ?? false)
|
|
1050
1083
|
case "deviceUuid":
|