@candypoets/nipworker 0.96.3 → 0.96.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +206 -73
  2. package/crates/native-ffi/README.md +14 -151
  3. package/crates/native-ffi/ios/README.md +4 -50
  4. package/crates/native-ffi/react-native/README.md +8 -7
  5. package/crates/native-ffi/react-native/android/build.gradle +1 -0
  6. package/crates/native-ffi/react-native/android/src/main/cpp/NipworkerByteRuntime.cpp +348 -0
  7. package/crates/native-ffi/react-native/android/src/main/java/com/candypoets/nipworker/reactnative/NipworkerHooks.kt +292 -0
  8. package/crates/native-ffi/react-native/android/src/main/java/com/candypoets/nipworker/reactnative/NipworkerReactNativeModule.kt +283 -89
  9. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/AddressPointer.java +75 -0
  10. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ArticleBlock.java +102 -0
  11. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ArticleBlockType.java +23 -0
  12. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ArticleEntity.java +80 -0
  13. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ArticleInline.java +85 -0
  14. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ArticleInlineType.java +24 -0
  15. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/BadgeAwardRecipient.java +61 -0
  16. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/BufferFull.java +52 -0
  17. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/CacheInput.java +16 -0
  18. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/CacheInputMessage.java +57 -0
  19. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/CacheRequest.java +84 -0
  20. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/CacheResponse.java +67 -0
  21. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/CashuData.java +55 -0
  22. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ChatLimiterPipeConfig.java +74 -0
  23. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/CodeData.java +61 -0
  24. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ConnectionStatus.java +68 -0
  25. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Contact.java +70 -0
  26. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ContentBlock.java +70 -0
  27. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ContentData.java +23 -0
  28. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Coordinate.java +75 -0
  29. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/CountResponse.java +60 -0
  30. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/CounterPipeConfig.java +67 -0
  31. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/DLEQProof.java +69 -0
  32. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Emoji.java +62 -0
  33. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/EmojiData.java +68 -0
  34. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Eoce.java +55 -0
  35. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/EventPointer.java +74 -0
  36. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/GetPublicKey.java +43 -0
  37. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/HTLCWitness.java +64 -0
  38. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/HashtagData.java +55 -0
  39. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/HistoryTag.java +74 -0
  40. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ImageData.java +67 -0
  41. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ImetaTag.java +100 -0
  42. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind0Parsed.java +168 -0
  43. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind10002Parsed.java +59 -0
  44. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind10019Parsed.java +73 -0
  45. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind1018Parsed.java +79 -0
  46. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind1068Parsed.java +107 -0
  47. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind1111Parsed.java +159 -0
  48. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind1311Parsed.java +91 -0
  49. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind17375Parsed.java +74 -0
  50. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind17Parsed.java +81 -0
  51. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind1Parsed.java +109 -0
  52. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind20Parsed.java +107 -0
  53. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind22Parsed.java +111 -0
  54. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind30023Parsed.java +113 -0
  55. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind3Parsed.java +59 -0
  56. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind4Parsed.java +78 -0
  57. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind6Parsed.java +53 -0
  58. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind7374Parsed.java +66 -0
  59. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind7375Parsed.java +79 -0
  60. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind7376Parsed.java +100 -0
  61. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind7Parsed.java +81 -0
  62. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind8Parsed.java +78 -0
  63. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind9321Parsed.java +117 -0
  64. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Kind9735Parsed.java +119 -0
  65. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/KindFilterPipeConfig.java +60 -0
  66. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/LinkPreviewData.java +73 -0
  67. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ListParsed.java +123 -0
  68. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/LiveActivityRef.java +72 -0
  69. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/LiveChatParticipant.java +61 -0
  70. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/LiveChatThreadRef.java +61 -0
  71. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/MainContent.java +20 -0
  72. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/MainMessage.java +57 -0
  73. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/MediaGroupData.java +58 -0
  74. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/MediaItem.java +58 -0
  75. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Message.java +24 -0
  76. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/MessageType.java +19 -0
  77. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/MintInfo.java +65 -0
  78. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/MintProofs.java +66 -0
  79. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/MuteFilterPipeConfig.java +84 -0
  80. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Nip07.java +43 -0
  81. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Nip46Bunker.java +61 -0
  82. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Nip46QR.java +61 -0
  83. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/NostrData.java +81 -0
  84. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/NostrEvent.java +95 -0
  85. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/NpubLimiterKey.java +15 -0
  86. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/NpubLimiterPipeConfig.java +64 -0
  87. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/P2PKWitness.java +57 -0
  88. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ParsePipeConfig.java +43 -0
  89. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ParsedData.java +39 -0
  90. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ParsedDataUnion.java +35 -0
  91. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ParsedEvent.java +108 -0
  92. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Pipe.java +57 -0
  93. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/PipeConfig.java +23 -0
  94. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/PipelineConfig.java +59 -0
  95. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/PollOption.java +62 -0
  96. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/PollType.java +15 -0
  97. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/PreGenericParsed.java +213 -0
  98. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/PreParticipant.java +73 -0
  99. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/PreRefEvent.java +67 -0
  100. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/PrivateKey.java +55 -0
  101. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ProfilePointer.java +64 -0
  102. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Proof.java +90 -0
  103. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ProofVerificationPipeConfig.java +52 -0
  104. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Pubkey.java +55 -0
  105. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Publish.java +80 -0
  106. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Raw.java +55 -0
  107. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ReactionType.java +17 -0
  108. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/RelayInfo.java +63 -0
  109. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Request.java +134 -0
  110. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SaveToDbPipeConfig.java +43 -0
  111. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SerializeEventsPipeConfig.java +55 -0
  112. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SetSigner.java +57 -0
  113. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SignEvent.java +54 -0
  114. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SignedEvent.java +54 -0
  115. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SignerOp.java +19 -0
  116. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SignerRequest.java +80 -0
  117. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SignerResponse.java +64 -0
  118. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SignerType.java +18 -0
  119. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/StringVec.java +57 -0
  120. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Subscribe.java +72 -0
  121. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SubscribeKind.java +19 -0
  122. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/SubscriptionConfig.java +95 -0
  123. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Tag.java +64 -0
  124. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Template.java +74 -0
  125. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Unsubscribe.java +55 -0
  126. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ValidProofs.java +59 -0
  127. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/VideoData.java +67 -0
  128. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/VideoVariant.java +102 -0
  129. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/Witness.java +17 -0
  130. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/WitnessString.java +54 -0
  131. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/WorkerMessage.java +74 -0
  132. package/crates/native-ffi/react-native/android/src/main/java/nostr/fb/ZapRequest.java +83 -0
  133. package/crates/native-ffi/react-native/ios/NipworkerReactNativeModule.h +18 -0
  134. package/crates/native-ffi/react-native/ios/NipworkerReactNativeModule.mm +263 -8
  135. package/dist/EngineManager.d.ts +1 -0
  136. package/dist/EngineManager.d.ts.map +1 -1
  137. package/dist/EngineManager.js +143 -137
  138. package/dist/NostrManager.d.ts +1 -0
  139. package/dist/NostrManager.d.ts.map +1 -1
  140. package/dist/NostrManager.js +49 -44
  141. package/dist/connections/index.js +87 -87
  142. package/dist/engine/index.js +119 -119
  143. package/dist/engine.d.ts.map +1 -1
  144. package/dist/hooks.d.ts +1 -1
  145. package/dist/hooks.d.ts.map +1 -1
  146. package/dist/hooks.js +27 -26
  147. package/dist/index.d.ts +1 -1
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/legacy.d.ts.map +1 -1
  150. package/dist/lib/ArrayBufferReader.d.ts +2 -1
  151. package/dist/lib/ArrayBufferReader.d.ts.map +1 -1
  152. package/dist/lib/BaseBackend.d.ts +5 -3
  153. package/dist/lib/BaseBackend.d.ts.map +1 -1
  154. package/dist/manager.d.ts +2 -6
  155. package/dist/manager.d.ts.map +1 -1
  156. package/dist/manager.js +10 -28
  157. package/dist/react-native.d.ts +33 -4
  158. package/dist/react-native.d.ts.map +1 -1
  159. package/dist/react-native.js +400 -122
  160. package/dist/scheduleMicrotask.js +58 -50
  161. package/package.json +13 -11
  162. package/swift/Package.swift +36 -0
  163. package/swift/README.md +438 -0
  164. package/swift/Sources/NipworkerSwift/ArrayBufferReader.swift +136 -0
  165. package/swift/Sources/NipworkerSwift/AuthSession.swift +24 -0
  166. package/swift/Sources/NipworkerSwift/FFI.swift +94 -0
  167. package/swift/Sources/NipworkerSwift/FlatBufferHelpers.swift +273 -0
  168. package/swift/Sources/NipworkerSwift/Generated/message_generated.swift +6530 -0
  169. package/swift/Sources/NipworkerSwift/Hooks.swift +300 -0
  170. package/swift/Sources/NipworkerSwift/Models.swift +156 -0
  171. package/swift/Sources/NipworkerSwift/NostrManager.swift +594 -0
  172. package/swift/Sources/NipworkerSwift/PublishTracker.swift +40 -0
  173. package/swift/Sources/NipworkerSwift/Subscription.swift +44 -0
  174. package/swift/Sources/NipworkerSwift/WorkerMessageView.swift +145 -0
  175. package/dist/NativeBackend.d.ts +0 -53
  176. package/dist/NativeBackend.d.ts.map +0 -1
  177. package/dist/NativeBackend.js +0 -464
  178. package/dist/native.d.ts +0 -29
  179. package/dist/native.d.ts.map +0 -1
  180. package/dist/native.js +0 -273
@@ -0,0 +1,594 @@
1
+ import Foundation
2
+ import Combine
3
+ import FlatBuffers
4
+
5
+ public extension Notification.Name {
6
+ static let nipworkerSubscriptionUpdated = Notification.Name("nipworkerSubscriptionUpdated")
7
+ static let nipworkerRelayStatusUpdated = Notification.Name("nipworkerRelayStatusUpdated")
8
+ static let nipworkerAuthUpdated = Notification.Name("nipworkerAuthUpdated")
9
+
10
+ static func nipworkerSubscriptionUpdated(subId: String) -> Notification.Name {
11
+ Notification.Name("nipworkerSubscriptionUpdated.\(subId)")
12
+ }
13
+ }
14
+
15
+ /// Weak reference box for the C callback
16
+ private final class ManagerBox {
17
+ weak var manager: NostrManager?
18
+ init(manager: NostrManager) { self.manager = manager }
19
+ }
20
+
21
+ public actor NostrManager {
22
+ private static let sharedRuntimeNotification = "NipworkerRuntimeDataNotification"
23
+ private static let sharedRuntimeDataKey = "data"
24
+
25
+ private static var reactNativeSharedManager: NostrManager?
26
+
27
+ private var handle: UnsafeMutableRawPointer?
28
+ private let ownsHandle: Bool
29
+ private var bufferViews: [String: SubscriptionBuffer] = [:]
30
+ private var relayStatuses: [String: RelayStatus] = [:]
31
+ private var activePubkey: String?
32
+ private var pendingSession: (type: String, payload: Any)?
33
+ private var sharedRuntimeObserver: NSObjectProtocol?
34
+
35
+ private var signContinuation: CheckedContinuation<NostrEvent, Error>?
36
+ private var pubkeyContinuation: CheckedContinuation<String, Error>?
37
+
38
+ private var boxPtr: UnsafeMutablePointer<ManagerBox>
39
+
40
+ public init(config: NostrManagerConfig = NostrManagerConfig()) {
41
+ self.handle = nil
42
+ self.ownsHandle = true
43
+ self.boxPtr = UnsafeMutablePointer<ManagerBox>.allocate(capacity: 1)
44
+ self.boxPtr.initialize(to: ManagerBox(manager: self))
45
+
46
+ self.handle = nipworker_init({ userdata, ptr, len in
47
+ guard let ptr = ptr else { return }
48
+ let data = Data(bytes: ptr, count: len)
49
+ nipworker_free_bytes(UnsafeMutablePointer(mutating: ptr), len)
50
+
51
+ let box = userdata!.assumingMemoryBound(to: ManagerBox.self).pointee
52
+ guard let manager = box.manager else { return }
53
+
54
+ Task {
55
+ await manager.handleNativeMessage(data)
56
+ }
57
+ }, self.boxPtr)
58
+ }
59
+
60
+ private init(borrowedHandle: UnsafeMutableRawPointer?) {
61
+ self.handle = borrowedHandle
62
+ self.ownsHandle = false
63
+ self.boxPtr = UnsafeMutablePointer<ManagerBox>.allocate(capacity: 1)
64
+ self.boxPtr.initialize(to: ManagerBox(manager: self))
65
+
66
+ self.sharedRuntimeObserver = NotificationCenter.default.addObserver(
67
+ forName: Notification.Name(rawValue: Self.sharedRuntimeNotification),
68
+ object: nil,
69
+ queue: .main
70
+ ) { [weak self] notification in
71
+ guard let data = notification.userInfo?[Self.sharedRuntimeDataKey] as? Data else { return }
72
+ Task { [weak self] in
73
+ await self?.handleNativeMessage(data)
74
+ }
75
+ }
76
+ }
77
+
78
+ public static func reactNativeShared() -> NostrManager {
79
+ if let manager = reactNativeSharedManager {
80
+ return manager
81
+ }
82
+ let manager = NostrManager(borrowedHandle: nipworker_react_native_shared_handle_if_available())
83
+ reactNativeSharedManager = manager
84
+ return manager
85
+ }
86
+
87
+ deinit {
88
+ if ownsHandle, let handle = handle {
89
+ nipworker_deinit(handle)
90
+ }
91
+ if let observer = sharedRuntimeObserver {
92
+ NotificationCenter.default.removeObserver(observer)
93
+ sharedRuntimeObserver = nil
94
+ }
95
+ boxPtr.pointee.manager = nil
96
+ boxPtr.deinitialize(count: 1)
97
+ boxPtr.deallocate()
98
+ }
99
+
100
+ // MARK: - Subscribe
101
+
102
+ public func subscribe(
103
+ subscriptionId: String,
104
+ requests: [RequestObject],
105
+ options: SubscriptionConfig = SubscriptionConfig()
106
+ ) -> SubscriptionBuffer {
107
+ let subId = subscriptionId
108
+ let existingRetained = subId.withCString { nipworker_retain_subscription(handle, $0) }
109
+ if existingRetained, let buffer = bufferView(for: subId) {
110
+ return buffer
111
+ }
112
+
113
+ let fbData = buildSubscribeMessage(subId: subId, requests: requests, options: options)
114
+ let ok = fbData.withUnsafeBytes { bytes in
115
+ nipworker_subscribe_message(
116
+ handle,
117
+ bytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
118
+ bytes.count
119
+ )
120
+ }
121
+
122
+ guard ok, let buffer = bufferView(for: subId) else {
123
+ return SubscriptionBuffer(capacity: 4)
124
+ }
125
+ return buffer
126
+ }
127
+
128
+ public func unsubscribe(subscriptionId: String) {
129
+ subscriptionId.withCString { nipworker_release_subscription(handle, $0) }
130
+ }
131
+
132
+ // MARK: - Publish
133
+
134
+ public func publish(
135
+ publishId: String,
136
+ event: NostrEvent,
137
+ defaultRelays: [String] = [],
138
+ optimisticSubIds: [String] = []
139
+ ) -> SubscriptionBuffer {
140
+ let fbData = buildPublishMessage(
141
+ publishId: publishId,
142
+ event: event,
143
+ defaultRelays: defaultRelays,
144
+ optimisticSubIds: optimisticSubIds
145
+ )
146
+ let ok = fbData.withUnsafeBytes { bytes in
147
+ nipworker_publish_message(
148
+ handle,
149
+ bytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
150
+ bytes.count
151
+ )
152
+ }
153
+
154
+ guard ok, let buffer = bufferView(for: publishId) else {
155
+ return SubscriptionBuffer(capacity: 4)
156
+ }
157
+ return buffer
158
+ }
159
+
160
+ public func releasePublish(publishId: String) {
161
+ publishId.withCString { nipworker_release_subscription(handle, $0) }
162
+ }
163
+
164
+ // MARK: - Signer
165
+
166
+ public func setSigner(_ signer: SignerTypeName) {
167
+ switch signer {
168
+ case .pubkey(let pubkey):
169
+ self.activePubkey = pubkey
170
+ DispatchQueue.main.async {
171
+ NotificationCenter.default.post(
172
+ name: .nipworkerAuthUpdated,
173
+ object: nil,
174
+ userInfo: ["pubkey": pubkey, "hasSigner": false]
175
+ )
176
+ }
177
+ case .privkey(let secret):
178
+ let fbData = buildSetPrivateKeyMessage(secret: secret)
179
+ fbData.withUnsafeBytes { bytes in
180
+ nipworker_handle_message(
181
+ handle,
182
+ bytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
183
+ bytes.count
184
+ )
185
+ }
186
+ Task {
187
+ _ = try? await getPublicKey()
188
+ }
189
+ }
190
+ }
191
+
192
+ public func getPublicKey() async throws -> String {
193
+ let fbData = buildGetPublicKeyMessage()
194
+ fbData.withUnsafeBytes { bytes in
195
+ nipworker_handle_message(
196
+ handle,
197
+ bytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
198
+ bytes.count
199
+ )
200
+ }
201
+
202
+ return try await withCheckedThrowingContinuation { continuation in
203
+ self.pubkeyContinuation = continuation
204
+ }
205
+ }
206
+
207
+ public func signEvent(_ template: EventTemplate) async throws -> NostrEvent {
208
+ let fbData = buildSignEventMessage(template: template)
209
+ fbData.withUnsafeBytes { bytes in
210
+ nipworker_handle_message(
211
+ handle,
212
+ bytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
213
+ bytes.count
214
+ )
215
+ }
216
+
217
+ return try await withCheckedThrowingContinuation { continuation in
218
+ self.signContinuation = continuation
219
+ }
220
+ }
221
+
222
+ public func logout() {
223
+ activePubkey = nil
224
+ pendingSession = nil
225
+ }
226
+
227
+ // MARK: - Cleanup
228
+
229
+ public func cleanup() {
230
+ nipworker_cleanup_subscriptions(handle)
231
+ }
232
+
233
+ // MARK: - Read Events (called by Subscription)
234
+
235
+ public func readEvents(for subId: String, from position: Int) -> (events: [NostrEvent], newPosition: Int) {
236
+ guard let buffer = bufferView(for: subId) else { return ([], position) }
237
+ let result = ArrayBufferReader.readMessages(buffer: buffer, lastReadPosition: position)
238
+ let events = result.messages.compactMap { parseWorkerMessage($0) }
239
+ return (events, result.newReadPosition)
240
+ }
241
+
242
+ public func readWorkerMessages(for subId: String, from position: Int) -> (messages: [WorkerMessageView], newPosition: Int) {
243
+ guard let buffer = bufferView(for: subId) else { return ([], position) }
244
+ let result = ArrayBufferReader.readMessages(buffer: buffer, lastReadPosition: position)
245
+ let messages = result.messages.compactMap { WorkerMessageView($0) }
246
+ return (messages, result.newReadPosition)
247
+ }
248
+
249
+ public func readPublishStatuses(for publishId: String, from position: Int) -> (statuses: [String: PublishStatus], newPosition: Int) {
250
+ guard let buffer = bufferView(for: publishId) else { return ([:], position) }
251
+ let result = ArrayBufferReader.readMessages(buffer: buffer, lastReadPosition: position)
252
+ var statuses: [String: PublishStatus] = [:]
253
+ for data in result.messages {
254
+ if let (url, status) = parsePublishStatus(data) {
255
+ statuses[url] = status
256
+ }
257
+ }
258
+ return (statuses, result.newReadPosition)
259
+ }
260
+
261
+ public func getRelayStatuses() -> [String: RelayStatus] {
262
+ return relayStatuses
263
+ }
264
+
265
+ public func getActivePubkey() -> String? {
266
+ return activePubkey
267
+ }
268
+
269
+ // MARK: - Callback Handling
270
+
271
+ private func handleNativeMessage(_ data: Data) {
272
+ if let routeSubId = decodeRouteWakeFrame(data) {
273
+ handleSubscriptionMessage(subId: routeSubId)
274
+ return
275
+ }
276
+ guard data.count >= 4 else { return }
277
+ let bb = ByteBuffer(data: data)
278
+ let rootOffset = bb.read(def: Int32.self, position: 0)
279
+ if rootOffset < 0 || Int(rootOffset) >= data.count { return }
280
+ let workerMsg = nostr_fb_WorkerMessage(bb, o: rootOffset)
281
+ let subId = workerMsg.subId ?? ""
282
+
283
+ if subId == "crypto" {
284
+ handleCryptoMessage(data)
285
+ } else if subId.isEmpty {
286
+ handleDirectResponse(data)
287
+ } else {
288
+ handleSubscriptionMessage(subId: subId)
289
+ }
290
+ }
291
+
292
+ private func handleSubscriptionMessage(subId: String) {
293
+ DispatchQueue.main.async {
294
+ NotificationCenter.default.post(
295
+ name: .nipworkerSubscriptionUpdated(subId: subId),
296
+ object: nil,
297
+ userInfo: ["subId": subId]
298
+ )
299
+ }
300
+ }
301
+
302
+ private func handleDirectResponse(_ payload: Data) {
303
+ guard payload.count >= 4 else { return }
304
+
305
+ let firstWord = Int(payload.withUnsafeBytes { $0.load(fromByteOffset: 0, as: UInt32.self).littleEndian })
306
+ if firstWord > 0, payload.count == 4 + firstWord {
307
+ _parseDirectResponse(payload.subdata(in: 4..<4 + firstWord))
308
+ } else {
309
+ _parseDirectResponse(payload)
310
+ }
311
+ }
312
+
313
+ private func handleCryptoMessage(_ payload: Data) {
314
+ _parseCryptoResponse(payload)
315
+ }
316
+
317
+ private func decodeRouteWakeFrame(_ data: Data) -> String? {
318
+ guard data.count >= 8 else { return nil }
319
+ guard [UInt8](data.prefix(4)) == [0x4e, 0x57, 0x52, 0x31] else { return nil }
320
+ let subIdLength = Int(data.withUnsafeBytes {
321
+ $0.load(fromByteOffset: 4, as: UInt32.self).littleEndian
322
+ })
323
+ guard subIdLength > 0, subIdLength == data.count - 8 else { return nil }
324
+ return String(data: data.subdata(in: 8..<data.count), encoding: .utf8)
325
+ }
326
+
327
+ // MARK: - Helpers
328
+
329
+ private func bufferView(for id: String) -> SubscriptionBuffer? {
330
+ let nativePointer = id.withCString { nipworker_subscription_buffer_ptr(handle, $0) }
331
+ let nativeLength = id.withCString { nipworker_subscription_buffer_len(handle, $0) }
332
+ guard let nativePointer, nativeLength > 0 else {
333
+ bufferViews.removeValue(forKey: id)
334
+ return nil
335
+ }
336
+ if let existing = bufferViews[id],
337
+ existing.pointer == UnsafeMutableRawPointer(nativePointer),
338
+ existing.capacity == nativeLength {
339
+ return existing
340
+ }
341
+ let buffer = SubscriptionBuffer(pointer: UnsafeMutableRawPointer(nativePointer), capacity: nativeLength)
342
+ bufferViews[id] = buffer
343
+ return buffer
344
+ }
345
+
346
+ public nonisolated func createShortId(_ input: String) -> String {
347
+ if input.count < 64 { return input }
348
+ var hash: Int32 = 0
349
+ for char in input.utf16 {
350
+ hash = (hash << 5) &- hash &+ Int32(char)
351
+ }
352
+ let shortId = String(Int(hash).magnitude, radix: 36)
353
+ return String(shortId.prefix(63))
354
+ }
355
+
356
+ // MARK: - FlatBuffer Parsing
357
+
358
+ private func parseWorkerMessage(_ data: Data) -> NostrEvent? {
359
+ let bb = ByteBuffer(data: data)
360
+ let rootOffset = bb.read(def: Int32.self, position: 0)
361
+ let workerMsg = nostr_fb_WorkerMessage(bb, o: rootOffset)
362
+
363
+ switch workerMsg.contentType {
364
+ case .nostrevent:
365
+ guard let event = workerMsg.content(type: nostr_fb_NostrEvent.self) else { return nil }
366
+ return NostrEvent(
367
+ id: event.id,
368
+ pubkey: event.pubkey,
369
+ kind: event.kind,
370
+ content: event.content,
371
+ tags: event.tags.map { $0.items.compactMap { $0 } },
372
+ createdAt: Int(event.createdAt),
373
+ sig: event.sig
374
+ )
375
+ case .parsedevent:
376
+ guard let parsed = workerMsg.content(type: nostr_fb_ParsedEvent.self) else { return nil }
377
+ return NostrEvent(
378
+ id: parsed.id,
379
+ pubkey: parsed.pubkey,
380
+ kind: parsed.kind,
381
+ content: "",
382
+ tags: parsed.tags.map { $0.items.compactMap { $0 } },
383
+ createdAt: Int(parsed.createdAt),
384
+ sig: ""
385
+ )
386
+ case .raw:
387
+ guard let raw = workerMsg.content(type: nostr_fb_Raw.self) else { return nil }
388
+ return parseRawEventMessage(raw.raw)
389
+ default:
390
+ return nil
391
+ }
392
+ }
393
+
394
+ private func parsePublishStatus(_ data: Data) -> (String, PublishStatus)? {
395
+ let bb = ByteBuffer(data: data)
396
+ let rootOffset = bb.read(def: Int32.self, position: 0)
397
+ let workerMsg = nostr_fb_WorkerMessage(bb, o: rootOffset)
398
+
399
+ switch workerMsg.contentType {
400
+ case .connectionstatus:
401
+ guard let status = workerMsg.content(type: nostr_fb_ConnectionStatus.self) else { return nil }
402
+ let relayUrl: String = status.relayUrl
403
+ let statusStr: String = status.status
404
+ let message: String = status.message ?? ""
405
+ switch statusStr {
406
+ case "ok":
407
+ return (relayUrl, .success(message))
408
+ case "failed":
409
+ return (relayUrl, .failed(message))
410
+ default:
411
+ return (relayUrl, .pending)
412
+ }
413
+ case .raw:
414
+ guard let raw = workerMsg.content(type: nostr_fb_Raw.self) else { return nil }
415
+ let text: String = raw.raw
416
+ if text.contains("\"OK\"") || text.contains("\"ok\"") {
417
+ return (text, .success(text))
418
+ } else if text.contains("NOTICE") {
419
+ return (text, .failed(text))
420
+ }
421
+ return nil
422
+ default:
423
+ return nil
424
+ }
425
+ }
426
+
427
+ private func _parseDirectResponse(_ data: Data) {
428
+ guard data.count >= 4 else { return }
429
+ let bb = ByteBuffer(data: data)
430
+ let rootOffset = bb.read(def: Int32.self, position: 0)
431
+ guard rootOffset >= 0, Int(rootOffset) < data.count else { return }
432
+ let workerMsg = nostr_fb_WorkerMessage(bb, o: rootOffset)
433
+
434
+ switch workerMsg.contentType {
435
+ case .pubkey:
436
+ guard let pubkey = workerMsg.content(type: nostr_fb_Pubkey.self) else { return }
437
+ let key: String = pubkey.pubkey
438
+ activePubkey = key
439
+ if let continuation = pubkeyContinuation {
440
+ pubkeyContinuation = nil
441
+ continuation.resume(returning: key)
442
+ }
443
+ DispatchQueue.main.async {
444
+ NotificationCenter.default.post(
445
+ name: .nipworkerAuthUpdated,
446
+ object: nil,
447
+ userInfo: ["pubkey": key, "hasSigner": true]
448
+ )
449
+ }
450
+ case .signedevent:
451
+ guard let signed = workerMsg.content(type: nostr_fb_SignedEvent.self),
452
+ let event = signed.event else { return }
453
+ let nostrEvent = NostrEvent(
454
+ id: event.id,
455
+ pubkey: event.pubkey,
456
+ kind: event.kind,
457
+ content: event.content,
458
+ tags: event.tags.map { $0.items.compactMap { $0 } },
459
+ createdAt: Int(event.createdAt),
460
+ sig: event.sig
461
+ )
462
+ if let continuation = signContinuation {
463
+ signContinuation = nil
464
+ continuation.resume(returning: nostrEvent)
465
+ }
466
+ case .connectionstatus:
467
+ guard let status = workerMsg.content(type: nostr_fb_ConnectionStatus.self) else { return }
468
+ let url: String = status.relayUrl
469
+ let statusStr: String = status.status
470
+ let message: String = status.message ?? ""
471
+ let relayStatus: RelayStatus
472
+ switch statusStr {
473
+ case "ok":
474
+ relayStatus = .connected
475
+ case "failed":
476
+ relayStatus = .failed
477
+ case "closed":
478
+ relayStatus = .closed(message.isEmpty ? nil : message)
479
+ default:
480
+ relayStatus = .failed
481
+ }
482
+ relayStatuses[url] = relayStatus
483
+ DispatchQueue.main.async {
484
+ NotificationCenter.default.post(
485
+ name: .nipworkerRelayStatusUpdated,
486
+ object: nil,
487
+ userInfo: ["url": url, "status": relayStatus]
488
+ )
489
+ }
490
+ default:
491
+ break
492
+ }
493
+ }
494
+
495
+ private func _parseCryptoResponse(_ data: Data) {
496
+ let bb = ByteBuffer(data: data)
497
+ let rootOffset = bb.read(def: Int32.self, position: 0)
498
+ let workerMsg = nostr_fb_WorkerMessage(bb, o: rootOffset)
499
+
500
+ guard workerMsg.contentType == .raw else { return }
501
+ guard let raw = workerMsg.content(type: nostr_fb_Raw.self) else { return }
502
+ let jsonStr: String = raw.raw
503
+
504
+ guard let jsonData = jsonStr.data(using: .utf8),
505
+ let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else { return }
506
+
507
+ let op = json["op"] as? String
508
+ let result = json["result"] as? String
509
+ let error = json["error"] as? String
510
+
511
+ if let error = error, !error.isEmpty {
512
+ if let continuation = pubkeyContinuation {
513
+ pubkeyContinuation = nil
514
+ continuation.resume(throwing: NostrManagerError.cryptoError(error))
515
+ }
516
+ if let continuation = signContinuation {
517
+ signContinuation = nil
518
+ continuation.resume(throwing: NostrManagerError.cryptoError(error))
519
+ }
520
+ return
521
+ }
522
+
523
+ switch op {
524
+ case "get_public_key":
525
+ if let key = result {
526
+ activePubkey = key
527
+ if let continuation = pubkeyContinuation {
528
+ pubkeyContinuation = nil
529
+ continuation.resume(returning: key)
530
+ }
531
+ DispatchQueue.main.async {
532
+ NotificationCenter.default.post(
533
+ name: .nipworkerAuthUpdated,
534
+ object: nil,
535
+ userInfo: ["pubkey": key, "hasSigner": true]
536
+ )
537
+ }
538
+ }
539
+ case "sign_event":
540
+ if let eventJson = result,
541
+ let eventData = eventJson.data(using: String.Encoding.utf8),
542
+ let eventDict = try? JSONSerialization.jsonObject(with: eventData) as? [String: Any] {
543
+ let event = parseEventDict(eventDict)
544
+ if let continuation = signContinuation {
545
+ signContinuation = nil
546
+ continuation.resume(returning: event)
547
+ }
548
+ }
549
+ default:
550
+ break
551
+ }
552
+ }
553
+
554
+ private func parseEventDict(_ dict: [String: Any]) -> NostrEvent {
555
+ return NostrEvent(
556
+ id: dict["id"] as? String ?? "",
557
+ pubkey: dict["pubkey"] as? String ?? "",
558
+ kind: uint16Value(dict["kind"]),
559
+ content: dict["content"] as? String ?? "",
560
+ tags: (dict["tags"] as? [[String]]) ?? [],
561
+ createdAt: intValue(dict["created_at"]),
562
+ sig: dict["sig"] as? String ?? ""
563
+ )
564
+ }
565
+
566
+ private func parseRawEventMessage(_ raw: String) -> NostrEvent? {
567
+ guard let data = raw.data(using: .utf8),
568
+ let array = try? JSONSerialization.jsonObject(with: data) as? [Any],
569
+ array.count >= 3,
570
+ let messageType = array[0] as? String,
571
+ messageType == "EVENT",
572
+ let eventDict = array[2] as? [String: Any] else {
573
+ return nil
574
+ }
575
+ return parseEventDict(eventDict)
576
+ }
577
+
578
+ private func uint16Value(_ value: Any?) -> UInt16 {
579
+ if let value = value as? UInt16 { return value }
580
+ if let value = value as? Int { return UInt16(value) }
581
+ if let value = value as? NSNumber { return value.uint16Value }
582
+ return 0
583
+ }
584
+
585
+ private func intValue(_ value: Any?) -> Int {
586
+ if let value = value as? Int { return value }
587
+ if let value = value as? NSNumber { return value.intValue }
588
+ return 0
589
+ }
590
+ }
591
+
592
+ public enum NostrManagerError: Error {
593
+ case cryptoError(String)
594
+ }
@@ -0,0 +1,40 @@
1
+ import Foundation
2
+ import Combine
3
+
4
+ @MainActor
5
+ public final class PublishTracker: ObservableObject {
6
+ @Published public private(set) var statuses: [String: PublishStatus] = [:]
7
+
8
+ public let publishId: String
9
+ private let manager: NostrManager
10
+ private var lastReadPosition: Int = 4
11
+ private var notificationToken: NSObjectProtocol?
12
+
13
+ public init(publishId: String, manager: NostrManager) {
14
+ self.publishId = publishId
15
+ self.manager = manager
16
+
17
+ self.notificationToken = NotificationCenter.default.addObserver(
18
+ forName: .nipworkerSubscriptionUpdated(subId: publishId),
19
+ object: nil,
20
+ queue: .main
21
+ ) { [weak self] _ in
22
+ guard let self = self else { return }
23
+ Task { await self.sync() }
24
+ }
25
+ }
26
+
27
+ deinit {
28
+ if let token = notificationToken {
29
+ NotificationCenter.default.removeObserver(token)
30
+ }
31
+ }
32
+
33
+ private func sync() async {
34
+ let result = await manager.readPublishStatuses(for: publishId, from: lastReadPosition)
35
+ lastReadPosition = result.newPosition
36
+ for (url, status) in result.statuses {
37
+ statuses[url] = status
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,44 @@
1
+ import Foundation
2
+ import Combine
3
+
4
+ @MainActor
5
+ public final class Subscription: ObservableObject {
6
+ @Published public private(set) var events: [NostrEvent] = []
7
+ @Published public private(set) var isComplete: Bool = false
8
+
9
+ public let id: String
10
+ private let manager: NostrManager
11
+ private var lastReadPosition: Int = 4
12
+ private var notificationToken: NSObjectProtocol?
13
+
14
+ public init(id: String, manager: NostrManager) {
15
+ self.id = id
16
+ self.manager = manager
17
+
18
+ self.notificationToken = NotificationCenter.default.addObserver(
19
+ forName: .nipworkerSubscriptionUpdated(subId: id),
20
+ object: nil,
21
+ queue: .main
22
+ ) { [weak self] _ in
23
+ guard let self = self else { return }
24
+ Task { await self.sync() }
25
+ }
26
+ }
27
+
28
+ deinit {
29
+ if let token = notificationToken {
30
+ NotificationCenter.default.removeObserver(token)
31
+ }
32
+ Task { [id, manager] in
33
+ await manager.unsubscribe(subscriptionId: id)
34
+ }
35
+ }
36
+
37
+ private func sync() async {
38
+ let result = await manager.readEvents(for: id, from: lastReadPosition)
39
+ lastReadPosition = result.newPosition
40
+ if !result.events.isEmpty {
41
+ events.append(contentsOf: result.events)
42
+ }
43
+ }
44
+ }