bleam 0.0.10 → 0.0.11

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 (68) hide show
  1. package/dist/ai.cjs +608 -59
  2. package/dist/ai.d.cts +217 -89
  3. package/dist/ai.d.ts +217 -89
  4. package/dist/ai.js +601 -59
  5. package/dist/cli.cjs +3 -183
  6. package/dist/cli.d.cts +2 -46
  7. package/dist/cli.d.ts +2 -46
  8. package/dist/cli.js +3 -183
  9. package/dist/files.d.cts +1 -1
  10. package/dist/files.d.ts +1 -1
  11. package/dist/platform.cjs +1 -13
  12. package/dist/platform.d.cts +1 -28
  13. package/dist/platform.d.ts +1 -28
  14. package/dist/platform.js +1 -10
  15. package/dist/schema.d.cts +1 -1
  16. package/dist/schema.d.ts +1 -1
  17. package/dist/state.d.cts +1 -1
  18. package/dist/state.d.ts +1 -1
  19. package/dist/{ui-Bg11tvlc.d.ts → ui-1WepaMS4.d.cts} +1 -1
  20. package/dist/{ui-Dd7SXdbg.d.cts → ui-CHc4xEs_.d.ts} +7 -7
  21. package/dist/ui.d.cts +1 -1
  22. package/dist/ui.d.ts +1 -1
  23. package/dist/window.d.cts +1 -1
  24. package/dist/window.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/templates/image-generation/app/index.tsx +3 -1
  27. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
  28. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
  29. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
  30. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
  31. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
  32. package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
  33. package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
  34. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
  35. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
  36. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
  37. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
  38. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
  39. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
  40. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
  41. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
  42. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
  43. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
  44. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
  45. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
  46. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
  47. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
  48. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
  49. package/templates/native/ios/Bleam/AppDelegate.swift +0 -206
  50. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -122
  51. package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
  52. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
  53. package/templates/native/ios/GenerationService/main.swift +174 -48
  54. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
  55. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
  56. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +225 -5
  57. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
  58. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -191
  59. package/templates/text-generation/app/index.tsx +93 -0
  60. package/templates/text-generation/app.config.ts +5 -0
  61. package/templates/updates/README.md +43 -199
  62. package/templates/updates/src/index.ts +5 -98
  63. package/templates/updates/src/schema.ts +18 -173
  64. package/templates/native/ios/PlatformHelper/main.swift +0 -726
  65. /package/dist/{files-DwA7pzr3.d.cts → files-4ZEoAWiv.d.ts} +0 -0
  66. /package/dist/{files-VrkQlKIT.d.ts → files-Dt5mbzLq.d.cts} +0 -0
  67. /package/dist/{schema-DOOjfXvs.d.ts → schema-CYh6n8GS.d.ts} +0 -0
  68. /package/dist/{schema-ENSMX_1t.d.cts → schema-oeOrd3i1.d.cts} +0 -0
@@ -15,7 +15,7 @@ public final class AIModule: Module {
15
15
 
16
16
  public func definition() -> ModuleDefinition {
17
17
  Name("AI")
18
- Events("imagegenProgress", "textGenerationUpdate", "toolCall")
18
+ Events("imagegenProgress", "textGenerationUpdate", "localTextGenerationUpdate", "toolCall")
19
19
 
20
20
  Function("isAvailable") { () -> Bool in
21
21
  #if canImport(FoundationModels)
@@ -147,13 +147,47 @@ public final class AIModule: Module {
147
147
  }
148
148
 
149
149
  AsyncFunction("cancelTextGeneration") { [weak self] (id: String) -> Bool in
150
- await self?.textGeneration.cancel(id) ?? false
150
+ let foundationCanceled = await self?.textGeneration.cancel(id) ?? false
151
+ let localCanceled = (try? await ImageGenerationServiceClient.shared.cancelText(id: id)) ?? false
152
+ return foundationCanceled || localCanceled
151
153
  }
152
154
 
153
155
  AsyncFunction("resolveToolCall") { [weak self] (request: ToolResultRequest) -> Bool in
154
156
  await self?.textGeneration.resolve(request) ?? false
155
157
  }
156
158
 
159
+ AsyncFunction("generateLocalText") { (request: LocalTextGenerationRequest, promise: Promise) in
160
+ Task { @MainActor in
161
+ do {
162
+ let event = try await ImageGenerationServiceClient.shared.runText(
163
+ request: Self.bonsaiTextRequest(from: request)
164
+ )
165
+ promise.resolve(event.tokenIDs)
166
+ } catch {
167
+ Self.rejectLocalTextGeneration(error, promise: promise)
168
+ }
169
+ }
170
+ }
171
+
172
+ AsyncFunction("streamLocalText") { [weak self] (request: LocalTextGenerationRequest, promise: Promise) in
173
+ Task { @MainActor in
174
+ do {
175
+ let event = try await ImageGenerationServiceClient.shared.runText(
176
+ request: Self.bonsaiTextRequest(from: request),
177
+ onUpdate: { [weak self] event in
178
+ self?.sendEvent("localTextGenerationUpdate", [
179
+ "id": event.id,
180
+ "tokenIds": event.tokenIDs,
181
+ ])
182
+ }
183
+ )
184
+ promise.resolve(event.tokenIDs)
185
+ } catch {
186
+ Self.rejectLocalTextGeneration(error, promise: promise)
187
+ }
188
+ }
189
+ }
190
+
157
191
  AsyncFunction("generateImage") { (request: ImageGenerationRequest, promise: Promise) in
158
192
  do {
159
193
  let imageRequest = try Self.imageRequest(from: request)
@@ -272,12 +306,23 @@ public final class AIModule: Module {
272
306
  guard !request.model.isEmpty else {
273
307
  throw ModuleError.invalidModel("Missing image model")
274
308
  }
275
- guard request.model == "flux2-klein-4b-8bit-abliterated" else {
309
+ guard request.model == "flux2-klein-4b-8bit-abliterated"
310
+ || request.model == "ternary-bonsai-image-4b"
311
+ else {
276
312
  throw ModuleError.invalidModel("Unsupported image model: \(request.model)")
277
313
  }
278
314
  guard !request.prompt.isEmpty else {
279
315
  throw ModuleError.invalidMessage("Expected prompt")
280
316
  }
317
+ if request.model == "ternary-bonsai-image-4b",
318
+ request.sourceImagePath != nil
319
+ || request.maskImagePath != nil
320
+ || request.outpaintWidth != nil
321
+ || !request.referenceImagePaths.isEmpty
322
+ || !request.loraPaths.isEmpty
323
+ {
324
+ throw ModuleError.invalidModel("Ternary Bonsai Image 4B currently supports text-to-image generation")
325
+ }
281
326
  if let sourceImagePath = request.sourceImagePath, sourceImagePath.isEmpty {
282
327
  throw ModuleError.invalidMessage("Source image path must not be empty")
283
328
  }
@@ -493,6 +538,40 @@ public final class AIModule: Module {
493
538
  )
494
539
  }
495
540
 
541
+ private static func bonsaiTextRequest(
542
+ from request: LocalTextGenerationRequest
543
+ ) throws -> BonsaiTextGenerationRequest {
544
+ guard !request.id.isEmpty else {
545
+ throw ModuleError.invalidMessage("Expected generation id")
546
+ }
547
+ guard !request.model.isEmpty else {
548
+ throw ModuleError.invalidModel("Expected Bonsai model id")
549
+ }
550
+ var isDirectory: ObjCBool = false
551
+ guard FileManager.default.fileExists(atPath: request.modelPath, isDirectory: &isDirectory),
552
+ isDirectory.boolValue
553
+ else {
554
+ throw ModuleError.invalidModel("Missing Bonsai model directory: \(request.modelPath)")
555
+ }
556
+ guard !request.inputIds.isEmpty else {
557
+ throw ModuleError.invalidMessage("Expected tokenized prompt")
558
+ }
559
+ return BonsaiTextGenerationRequest(
560
+ id: request.id,
561
+ model: request.model,
562
+ modelPath: request.modelPath,
563
+ inputIDs: request.inputIds,
564
+ endTokenIDs: request.endTokenIds,
565
+ maximumNewTokens: request.maxTokens,
566
+ temperature: request.temperature,
567
+ topP: request.topP,
568
+ topK: request.topK,
569
+ repetitionPenalty: request.repetitionPenalty,
570
+ repetitionContextSize: request.repetitionContextSize,
571
+ seed: request.seed
572
+ )
573
+ }
574
+
496
575
  private static func metadataDictionary(_ metadata: GenerationWorkerMetadata?) -> [String: Any]? {
497
576
  guard let metadata else { return nil }
498
577
  var value: [String: Any] = [
@@ -530,6 +609,22 @@ public final class AIModule: Module {
530
609
  promise.reject(error)
531
610
  }
532
611
 
612
+ private static func rejectLocalTextGeneration(_ error: Error, promise: Promise) {
613
+ if error is CancellationError {
614
+ promise.reject("GENERATION_CANCELED", "Generation canceled")
615
+ return
616
+ }
617
+ if error as? GenerationServiceClientError == .concurrentRequest {
618
+ promise.reject("CONCURRENT_REQUEST", error.localizedDescription)
619
+ return
620
+ }
621
+ if let failure = error as? GenerationWorkerFailure {
622
+ promise.reject("GENERATION_FAILED", failure.localizedDescription)
623
+ return
624
+ }
625
+ promise.reject(error)
626
+ }
627
+
533
628
  #if canImport(FoundationModels)
534
629
  @available(iOS 26, *)
535
630
  private static func session(system: String?, tools: [any Tool]) -> LanguageModelSession {
@@ -849,6 +944,7 @@ private final class ImageGenerationServiceClient {
849
944
  }
850
945
  private var connection: NSXPCConnection?
851
946
  private var pendingRun: PendingRun?
947
+ private var pendingTextRun: PendingTextRun?
852
948
  private let eventReceiver = GenerationServiceEventReceiver()
853
949
 
854
950
  private final class PendingRun {
@@ -864,13 +960,29 @@ private final class ImageGenerationServiceClient {
864
960
  }
865
961
  }
866
962
 
963
+ private final class PendingTextRun {
964
+ let id: String
965
+ let continuation: CheckedContinuation<BonsaiTextGenerationEvent, Error>
966
+ let onUpdate: (BonsaiTextGenerationEvent) -> Void
967
+
968
+ init(
969
+ id: String,
970
+ continuation: CheckedContinuation<BonsaiTextGenerationEvent, Error>,
971
+ onUpdate: @escaping (BonsaiTextGenerationEvent) -> Void
972
+ ) {
973
+ self.id = id
974
+ self.continuation = continuation
975
+ self.onUpdate = onUpdate
976
+ }
977
+ }
978
+
867
979
  func run(
868
980
  request: GenerationWorkerRequest,
869
981
  onProgress: @escaping (GenerationWorkerEvent) -> Void = { _ in }
870
982
  ) async throws -> GenerationWorkerEvent {
871
983
  let terminalEvent: GenerationWorkerEvent = try await withCheckedThrowingContinuation { continuation in
872
984
  do {
873
- guard pendingRun == nil else {
985
+ guard pendingRun == nil, pendingTextRun == nil else {
874
986
  continuation.resume(throwing: GenerationServiceClientError.concurrentRequest)
875
987
  return
876
988
  }
@@ -905,6 +1017,71 @@ private final class ImageGenerationServiceClient {
905
1017
  return terminalEvent
906
1018
  }
907
1019
 
1020
+ func runText(
1021
+ request: BonsaiTextGenerationRequest,
1022
+ onUpdate: @escaping (BonsaiTextGenerationEvent) -> Void = { _ in }
1023
+ ) async throws -> BonsaiTextGenerationEvent {
1024
+ let terminalEvent: BonsaiTextGenerationEvent = try await withCheckedThrowingContinuation { continuation in
1025
+ do {
1026
+ guard pendingRun == nil, pendingTextRun == nil else {
1027
+ continuation.resume(throwing: GenerationServiceClientError.concurrentRequest)
1028
+ return
1029
+ }
1030
+ eventReceiver.onTextEvent = { [weak self] event in
1031
+ self?.deliverText(event)
1032
+ }
1033
+ pendingTextRun = PendingTextRun(
1034
+ id: request.id,
1035
+ continuation: continuation,
1036
+ onUpdate: onUpdate
1037
+ )
1038
+ let requestData = try JSONEncoder().encode(request)
1039
+ let service = try remoteProxy(errorHandler: { [weak self] error in
1040
+ Task { @MainActor in
1041
+ self?.failPendingTextRun(Self.mapConnectionError(error))
1042
+ }
1043
+ })
1044
+ service.startTextGeneration(requestData: requestData) { [weak self] accepted in
1045
+ Task { @MainActor in
1046
+ guard accepted else {
1047
+ self?.failPendingTextRun(GenerationServiceClientError.concurrentRequest)
1048
+ return
1049
+ }
1050
+ }
1051
+ }
1052
+ } catch {
1053
+ pendingTextRun = nil
1054
+ eventReceiver.onTextEvent = nil
1055
+ continuation.resume(throwing: error)
1056
+ }
1057
+ }
1058
+
1059
+ switch terminalEvent.type {
1060
+ case .completed:
1061
+ return terminalEvent
1062
+ case .canceled:
1063
+ throw CancellationError()
1064
+ case .failed:
1065
+ throw GenerationWorkerFailure(message: terminalEvent.message ?? "Text generation failed.")
1066
+ case .update:
1067
+ throw GenerationServiceClientError.invalidResponse
1068
+ }
1069
+ }
1070
+
1071
+ func cancelText(id: String) async throws -> Bool {
1072
+ guard pendingTextRun?.id == id else { return false }
1073
+ let service = try remoteProxy(errorHandler: { [weak self] error in
1074
+ Task { @MainActor in
1075
+ self?.failPendingTextRun(Self.mapConnectionError(error))
1076
+ }
1077
+ })
1078
+ return try await withCheckedThrowingContinuation { continuation in
1079
+ service.cancelTextGeneration(id: id) { canceled in
1080
+ continuation.resume(returning: canceled)
1081
+ }
1082
+ }
1083
+ }
1084
+
908
1085
  func cancel() async throws -> Bool {
909
1086
  guard pendingRun != nil else {
910
1087
  return false
@@ -940,7 +1117,7 @@ private final class ImageGenerationServiceClient {
940
1117
  }
941
1118
 
942
1119
  func unload() async throws -> Bool {
943
- guard pendingRun == nil else { return false }
1120
+ guard pendingRun == nil, pendingTextRun == nil else { return false }
944
1121
  let service = try remoteProxy(errorHandler: { _ in })
945
1122
  let unloaded = try await withCheckedThrowingContinuation { continuation in
946
1123
  service.shutdown { accepted in
@@ -1025,6 +1202,17 @@ private final class ImageGenerationServiceClient {
1025
1202
  }
1026
1203
  }
1027
1204
 
1205
+ private func deliverText(_ event: BonsaiTextGenerationEvent) {
1206
+ guard let pendingTextRun, event.id == pendingTextRun.id else { return }
1207
+ if event.type == .update {
1208
+ pendingTextRun.onUpdate(event)
1209
+ return
1210
+ }
1211
+ self.pendingTextRun = nil
1212
+ eventReceiver.onTextEvent = nil
1213
+ pendingTextRun.continuation.resume(returning: event)
1214
+ }
1215
+
1028
1216
  private func failPendingRun(_ error: Error) {
1029
1217
  guard let pendingRun else { return }
1030
1218
  self.pendingRun = nil
@@ -1035,9 +1223,17 @@ private final class ImageGenerationServiceClient {
1035
1223
  pendingRun.continuation.resume(throwing: error)
1036
1224
  }
1037
1225
 
1226
+ private func failPendingTextRun(_ error: Error) {
1227
+ guard let pendingTextRun else { return }
1228
+ self.pendingTextRun = nil
1229
+ eventReceiver.onTextEvent = nil
1230
+ pendingTextRun.continuation.resume(throwing: error)
1231
+ }
1232
+
1038
1233
  private func handleConnectionLost() {
1039
1234
  connection = nil
1040
1235
  failPendingRun(GenerationServiceClientError.serviceInterrupted)
1236
+ failPendingTextRun(GenerationServiceClientError.serviceInterrupted)
1041
1237
  }
1042
1238
 
1043
1239
  private func invalidate() {
@@ -1150,6 +1346,7 @@ private enum GenerationServiceClientError: LocalizedError, Equatable {
1150
1346
 
1151
1347
  private final class GenerationServiceEventReceiver: NSObject, GenerationServiceClientProtocol {
1152
1348
  @MainActor var onEvent: ((GenerationWorkerEvent) -> Void)?
1349
+ @MainActor var onTextEvent: ((BonsaiTextGenerationEvent) -> Void)?
1153
1350
 
1154
1351
  func generationWorkerDidEmit(eventData: Data) {
1155
1352
  guard let event = try? JSONDecoder().decode(GenerationWorkerEvent.self, from: eventData) else { return }
@@ -1157,6 +1354,14 @@ private final class GenerationServiceEventReceiver: NSObject, GenerationServiceC
1157
1354
  onEvent?(event)
1158
1355
  }
1159
1356
  }
1357
+
1358
+
1359
+ func textGenerationWorkerDidEmit(eventData: Data) {
1360
+ guard let event = try? JSONDecoder().decode(BonsaiTextGenerationEvent.self, from: eventData) else { return }
1361
+ Task { @MainActor in
1362
+ onTextEvent?(event)
1363
+ }
1364
+ }
1160
1365
  }
1161
1366
 
1162
1367
  private struct TextGenerationRequest: Record {
@@ -1169,6 +1374,21 @@ private struct TextGenerationRequest: Record {
1169
1374
  @Field var tools: [ToolDefinitionRequest] = []
1170
1375
  }
1171
1376
 
1377
+ private struct LocalTextGenerationRequest: Record {
1378
+ @Field var id: String = ""
1379
+ @Field var model: String = ""
1380
+ @Field var modelPath: String = ""
1381
+ @Field var inputIds: [Int] = []
1382
+ @Field var endTokenIds: [Int] = []
1383
+ @Field var maxTokens: Int?
1384
+ @Field var temperature: Double?
1385
+ @Field var topP: Double?
1386
+ @Field var topK: Int?
1387
+ @Field var repetitionPenalty: Double?
1388
+ @Field var repetitionContextSize: Int?
1389
+ @Field var seed: Int?
1390
+ }
1391
+
1172
1392
  private struct ToolDefinitionRequest: Record {
1173
1393
  @Field var name: String = ""
1174
1394
  @Field var description: String = ""
@@ -3,12 +3,15 @@ import Foundation
3
3
  @objc protocol GenerationServiceProtocol {
4
4
  func startGeneration(requestData: Data, reply: @escaping (Bool) -> Void)
5
5
  func cancelGeneration(reply: @escaping (Bool) -> Void)
6
+ func startTextGeneration(requestData: Data, reply: @escaping (Bool) -> Void)
7
+ func cancelTextGeneration(id: String, reply: @escaping (Bool) -> Void)
6
8
  func memoryTelemetry(reply: @escaping (Data?) -> Void)
7
9
  func shutdown(reply: @escaping (Bool) -> Void)
8
10
  }
9
11
 
10
12
  @objc protocol GenerationServiceClientProtocol {
11
13
  func generationWorkerDidEmit(eventData: Data)
14
+ func textGenerationWorkerDidEmit(eventData: Data)
12
15
  }
13
16
 
14
17
  struct GenerationWorkerRequest: Codable, Sendable {
@@ -133,3 +136,47 @@ struct GenerationWorkerLoRA: Codable, Sendable {
133
136
  let path: String
134
137
  let scale: Double
135
138
  }
139
+
140
+ struct BonsaiTextGenerationRequest: Codable, Sendable {
141
+ let id: String
142
+ let model: String
143
+ let modelPath: String
144
+ let inputIDs: [Int]
145
+ let endTokenIDs: [Int]
146
+ let maximumNewTokens: Int?
147
+ let temperature: Double?
148
+ let topP: Double?
149
+ let topK: Int?
150
+ let repetitionPenalty: Double?
151
+ let repetitionContextSize: Int?
152
+ let seed: Int?
153
+ }
154
+
155
+ struct BonsaiTextGenerationEvent: Codable, Sendable {
156
+ enum EventType: String, Codable, Sendable {
157
+ case update
158
+ case completed
159
+ case canceled
160
+ case failed
161
+ }
162
+
163
+ let type: EventType
164
+ let id: String
165
+ let tokenIDs: [Int]
166
+ let finishReason: String?
167
+ let message: String?
168
+
169
+ init(
170
+ type: EventType,
171
+ id: String,
172
+ tokenIDs: [Int] = [],
173
+ finishReason: String? = nil,
174
+ message: String? = nil
175
+ ) {
176
+ self.type = type
177
+ self.id = id
178
+ self.tokenIDs = tokenIDs
179
+ self.finishReason = finishReason
180
+ self.message = message
181
+ }
182
+ }
@@ -6,70 +6,10 @@ import ExpoModulesCore
6
6
  import Foundation
7
7
  import Photos
8
8
 
9
- @objc private protocol PlatformHelperProtocol {
10
- func health(reply: @escaping (Data?) -> Void)
11
- func downloadPlatformUpdate(requestData: Data, reply: @escaping (Data?) -> Void)
12
- func applyPlatformUpdate(requestData: Data, reply: @escaping (Bool) -> Void)
13
- }
14
-
15
9
  public final class PlatformModule: Module {
16
10
  public func definition() -> ModuleDefinition {
17
11
  Name("Platform")
18
12
 
19
- AsyncFunction("helperStatus") { (promise: Promise) in
20
- guard Self.isSupportedInstallLocation else {
21
- UserDefaults.standard.set(false, forKey: "BleamPlatformUpdatesSupported")
22
- promise.resolve([
23
- "healthy": false,
24
- "protocolVersion": 1,
25
- "helperVersion": "unavailable",
26
- "platformVersion": "unavailable",
27
- "capabilities": [],
28
- "updatesSupported": false,
29
- "installPath": Bundle.main.bundleURL.path,
30
- "reason": "reinstall_required",
31
- ])
32
- return
33
- }
34
- let serviceName = "\(Bundle.main.bundleIdentifier ?? "dev.bleam.app").PlatformHelper"
35
- guard let connection = Self.makeConnection(serviceName: serviceName) else {
36
- promise.reject("HELPER_UNAVAILABLE", "The app helper is unavailable")
37
- return
38
- }
39
- connection.remoteObjectInterface = NSXPCInterface(with: PlatformHelperProtocol.self)
40
- connection.resume()
41
-
42
- guard let service = connection.remoteObjectProxyWithErrorHandler({ error in
43
- connection.invalidate()
44
- promise.reject("HELPER_UNAVAILABLE", error.localizedDescription)
45
- }) as? PlatformHelperProtocol else {
46
- connection.invalidate()
47
- promise.reject("HELPER_UNAVAILABLE", "The app helper is unavailable")
48
- return
49
- }
50
-
51
- service.health { data in
52
- defer { connection.invalidate() }
53
- guard
54
- let data,
55
- var status = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
56
- else {
57
- promise.reject("HELPER_INVALID_RESPONSE", "The app helper returned an invalid response")
58
- return
59
- }
60
- let supported = Self.isSupportedInstallLocation
61
- status["updatesSupported"] = supported
62
- status["installPath"] = Bundle.main.bundleURL.path
63
- if !supported {
64
- status["reason"] = "reinstall_required"
65
- }
66
- UserDefaults.standard.set(status["healthy"], forKey: "BleamHelperHealthy")
67
- UserDefaults.standard.set(status["platformVersion"], forKey: "BleamHelperPlatformVersion")
68
- UserDefaults.standard.set(supported, forKey: "BleamPlatformUpdatesSupported")
69
- promise.resolve(status)
70
- }
71
- }
72
-
73
13
  AsyncFunction("permissionState") { (capability: String, promise: Promise) in
74
14
  guard let state = Self.permissionState(capability) else {
75
15
  promise.reject("INVALID_CAPABILITY", "Unknown permission capability: \(capability)")
@@ -85,132 +25,6 @@ public final class PlatformModule: Module {
85
25
  }
86
26
  promise.resolve(states)
87
27
  }
88
-
89
- AsyncFunction("downloadPlatformUpdate") { (promise: Promise) in
90
- guard Self.isSupportedInstallLocation else {
91
- promise.reject("REINSTALL_REQUIRED", "Platform updates require the app to be installed directly in ~/Applications")
92
- return
93
- }
94
- guard
95
- let serviceURL = Bundle.main.object(forInfoDictionaryKey: "BleamUpdateServiceURL") as? String,
96
- let bundleIdentifier = Bundle.main.bundleIdentifier,
97
- let teamIdentifier = Bundle.main.object(forInfoDictionaryKey: "BleamTeamIdentifier") as? String,
98
- let platformVersion = Bundle.main.object(forInfoDictionaryKey: "BleamRuntimeVersion") as? String,
99
- let platformFingerprint = Bundle.main.object(forInfoDictionaryKey: "BleamPlatformFingerprint") as? String,
100
- let requestData = try? JSONSerialization.data(withJSONObject: [
101
- "serviceURL": serviceURL,
102
- "bundleIdentifier": bundleIdentifier,
103
- "teamIdentifier": teamIdentifier,
104
- "platformVersion": platformVersion,
105
- "platformFingerprint": platformFingerprint,
106
- ])
107
- else {
108
- promise.reject("UPDATES_UNAVAILABLE", "Platform updates are not configured")
109
- return
110
- }
111
-
112
- guard let connection = Self.makeConnection(serviceName: "\(bundleIdentifier).PlatformHelper") else {
113
- promise.reject("HELPER_UNAVAILABLE", "The app helper is unavailable")
114
- return
115
- }
116
- connection.remoteObjectInterface = NSXPCInterface(with: PlatformHelperProtocol.self)
117
- connection.resume()
118
- guard let service = connection.remoteObjectProxyWithErrorHandler({ error in
119
- connection.invalidate()
120
- promise.reject("PLATFORM_DOWNLOAD_FAILED", error.localizedDescription)
121
- }) as? PlatformHelperProtocol else {
122
- connection.invalidate()
123
- promise.reject("HELPER_UNAVAILABLE", "The app helper is unavailable")
124
- return
125
- }
126
-
127
- service.downloadPlatformUpdate(requestData: requestData) { data in
128
- defer { connection.invalidate() }
129
- guard
130
- let data,
131
- let result = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
132
- else {
133
- promise.reject("PLATFORM_DOWNLOAD_FAILED", "The platform update could not be downloaded")
134
- return
135
- }
136
- if let error = result["error"] as? String {
137
- UserDefaults.standard.set(error, forKey: "BleamPlatformDownloadError")
138
- promise.reject("PLATFORM_VERIFICATION_FAILED", error)
139
- return
140
- }
141
- UserDefaults.standard.set(result["releaseId"], forKey: "BleamPlatformDownloadRelease")
142
- promise.resolve(result)
143
- }
144
- }
145
-
146
- AsyncFunction("updateAndRelaunch") { (releaseId: String, promise: Promise) in
147
- guard Self.isSupportedInstallLocation else {
148
- promise.reject("REINSTALL_REQUIRED", "Platform updates require the app to be installed directly in ~/Applications")
149
- return
150
- }
151
- guard
152
- let bundleIdentifier = Bundle.main.bundleIdentifier,
153
- let teamIdentifier = Bundle.main.object(forInfoDictionaryKey: "BleamTeamIdentifier") as? String,
154
- let requestData = try? JSONSerialization.data(withJSONObject: [
155
- "releaseId": releaseId,
156
- "bundleIdentifier": bundleIdentifier,
157
- "teamIdentifier": teamIdentifier,
158
- "installedAppPath": Bundle.main.bundleURL.path,
159
- "appPID": ProcessInfo.processInfo.processIdentifier,
160
- ]),
161
- let connection = Self.makeConnection(serviceName: "\(bundleIdentifier).PlatformHelper")
162
- else {
163
- promise.reject("PLATFORM_UPDATE_UNAVAILABLE", "The platform update cannot be applied")
164
- return
165
- }
166
- connection.remoteObjectInterface = NSXPCInterface(with: PlatformHelperProtocol.self)
167
- connection.resume()
168
- guard let service = connection.remoteObjectProxyWithErrorHandler({ error in
169
- connection.invalidate()
170
- promise.reject("PLATFORM_UPDATE_FAILED", error.localizedDescription)
171
- }) as? PlatformHelperProtocol else {
172
- connection.invalidate()
173
- promise.reject("HELPER_UNAVAILABLE", "The app helper is unavailable")
174
- return
175
- }
176
- service.applyPlatformUpdate(requestData: requestData) { accepted in
177
- connection.invalidate()
178
- UserDefaults.standard.set(accepted, forKey: "BleamPlatformApplyAccepted")
179
- guard accepted else {
180
- promise.reject("PLATFORM_UPDATE_REJECTED", "The platform update is not verified or the app is not installed in ~/Applications")
181
- return
182
- }
183
- promise.resolve(nil)
184
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
185
- exit(0)
186
- }
187
- }
188
- }
189
- }
190
-
191
- private static func makeConnection(serviceName: String) -> NSXPCConnection? {
192
- #if targetEnvironment(macCatalyst)
193
- let connectionClass: AnyObject = NSXPCConnection.self as AnyObject
194
- let allocated = connectionClass.perform(NSSelectorFromString("alloc"))?.takeUnretainedValue() as AnyObject?
195
- let selector = NSSelectorFromString("initWithMachServiceName:options:")
196
- guard let allocated, allocated.responds(to: selector) else { return nil }
197
-
198
- typealias Initializer = @convention(c) (
199
- AnyObject,
200
- Selector,
201
- NSString,
202
- UInt
203
- ) -> Unmanaged<AnyObject>
204
- let initialize = unsafeBitCast(allocated.method(for: selector), to: Initializer.self)
205
- return initialize(
206
- allocated,
207
- selector,
208
- serviceName as NSString,
209
- 0
210
- ).takeRetainedValue() as? NSXPCConnection
211
- #else
212
- return NSXPCConnection(machServiceName: serviceName)
213
- #endif
214
28
  }
215
29
 
216
30
  private static let permissionCapabilities = [
@@ -321,9 +135,4 @@ public final class PlatformModule: Module {
321
135
  }
322
136
  }
323
137
 
324
- private static var isSupportedInstallLocation: Bool {
325
- let applications = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
326
- .appendingPathComponent("Applications", isDirectory: true).standardizedFileURL
327
- return Bundle.main.bundleURL.standardizedFileURL.deletingLastPathComponent() == applications
328
- }
329
138
  }