@dr33m/react-native-litert-lm 0.5.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/LICENSE +21 -0
- package/README.md +627 -0
- package/android/CMakeLists.txt +32 -0
- package/android/build.gradle +104 -0
- package/android/src/main/AndroidManifest.xml +19 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +888 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridModelStore.kt +189 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/LiteRTLMRegistry.kt +32 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/MultimodalPartFactories.kt +15 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/StreamingCallbackListener.kt +89 -0
- package/android/src/main/java/dev/litert/litertlm/LiteRTLMInitProvider.kt +57 -0
- package/android/src/main/java/dev/litert/litertlm/LiteRTLMPackage.kt +43 -0
- package/android/src/test/java/com/margelo/nitro/core/Promise.kt +46 -0
- package/android/src/test/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMTest.kt +122 -0
- package/app.plugin.js +58 -0
- package/cpp/cpp-adapter.cpp +21 -0
- package/cpp/include/README.md +32 -0
- package/cpp/include/litert_lm_engine.h +516 -0
- package/cpp/include/stb_image.h +7988 -0
- package/ios/HybridLiteRTLM+Execute.swift +287 -0
- package/ios/HybridLiteRTLM+Streaming.swift +153 -0
- package/ios/HybridLiteRTLM.swift +541 -0
- package/ios/HybridModelStore.swift +206 -0
- package/ios/MultimodalPart+Factories.swift +22 -0
- package/ios/Tests/HybridLiteRTLMTests.swift +187 -0
- package/lib/__mocks__/react-native-nitro-modules.d.ts +81 -0
- package/lib/__mocks__/react-native-nitro-modules.js +98 -0
- package/lib/__tests__/hooks.test.d.ts +1 -0
- package/lib/__tests__/hooks.test.js +175 -0
- package/lib/__tests__/inferenceRouting.test.d.ts +1 -0
- package/lib/__tests__/inferenceRouting.test.js +28 -0
- package/lib/__tests__/memoryTracker.test.d.ts +1 -0
- package/lib/__tests__/memoryTracker.test.js +74 -0
- package/lib/__tests__/modelFactory.test.d.ts +1 -0
- package/lib/__tests__/modelFactory.test.js +107 -0
- package/lib/__tests__/modelPath.test.d.ts +1 -0
- package/lib/__tests__/modelPath.test.js +12 -0
- package/lib/__tests__/modelRegistry.test.d.ts +1 -0
- package/lib/__tests__/modelRegistry.test.js +61 -0
- package/lib/hooks.d.ts +46 -0
- package/lib/hooks.js +159 -0
- package/lib/index.d.ts +142 -0
- package/lib/index.js +184 -0
- package/lib/inferenceRouting.d.ts +18 -0
- package/lib/inferenceRouting.js +63 -0
- package/lib/memoryTracker.d.ts +128 -0
- package/lib/memoryTracker.js +155 -0
- package/lib/modelFactory.d.ts +24 -0
- package/lib/modelFactory.js +128 -0
- package/lib/modelPath.d.ts +8 -0
- package/lib/modelPath.js +18 -0
- package/lib/modelRegistry.d.ts +55 -0
- package/lib/modelRegistry.js +82 -0
- package/lib/specs/LiteRTLM.nitro.d.ts +380 -0
- package/lib/specs/LiteRTLM.nitro.js +2 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/LiteRTLM+autolinking.cmake +83 -0
- package/nitrogen/generated/android/LiteRTLM+autolinking.gradle +27 -0
- package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +74 -0
- package/nitrogen/generated/android/LiteRTLMOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JBackend.hpp +61 -0
- package/nitrogen/generated/android/c++/JExecuteResult.hpp +86 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_bool.hpp +76 -0
- package/nitrogen/generated/android/c++/JGenerationStats.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +371 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +83 -0
- package/nitrogen/generated/android/c++/JHybridModelStoreSpec.cpp +101 -0
- package/nitrogen/generated/android/c++/JHybridModelStoreSpec.hpp +67 -0
- package/nitrogen/generated/android/c++/JLLMConfig.hpp +125 -0
- package/nitrogen/generated/android/c++/JMemoryUsage.hpp +69 -0
- package/nitrogen/generated/android/c++/JMessage.hpp +63 -0
- package/nitrogen/generated/android/c++/JModelFile.hpp +69 -0
- package/nitrogen/generated/android/c++/JMultimodalPart.hpp +78 -0
- package/nitrogen/generated/android/c++/JPartType.hpp +61 -0
- package/nitrogen/generated/android/c++/JRole.hpp +61 -0
- package/nitrogen/generated/android/c++/JToolCall.hpp +61 -0
- package/nitrogen/generated/android/c++/JToolDefinition.hpp +65 -0
- package/nitrogen/generated/android/c++/JToolResponse.hpp +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Backend.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ExecuteResult.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_std__string_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/GenerationStats.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +165 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridModelStoreSpec.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +106 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LiteRTLMOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Message.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ModelFile.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MultimodalPart.kt +71 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/PartType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Role.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolCall.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolDefinition.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolResponse.kt +56 -0
- package/nitrogen/generated/ios/LiteRTLM+autolinking.rb +62 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.cpp +98 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.hpp +585 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Umbrella.hpp +93 -0
- package/nitrogen/generated/ios/LiteRTLMAutolinking.mm +41 -0
- package/nitrogen/generated/ios/LiteRTLMAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +279 -0
- package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.hpp +117 -0
- package/nitrogen/generated/ios/swift/Backend.swift +44 -0
- package/nitrogen/generated/ios/swift/ExecuteResult.swift +45 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ExecuteResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/GenerationStats.swift +54 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +75 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +516 -0
- package/nitrogen/generated/ios/swift/HybridModelStoreSpec.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridModelStoreSpec_cxx.swift +203 -0
- package/nitrogen/generated/ios/swift/LLMConfig.swift +239 -0
- package/nitrogen/generated/ios/swift/MemoryUsage.swift +44 -0
- package/nitrogen/generated/ios/swift/Message.swift +34 -0
- package/nitrogen/generated/ios/swift/ModelFile.swift +44 -0
- package/nitrogen/generated/ios/swift/MultimodalPart.swift +101 -0
- package/nitrogen/generated/ios/swift/PartType.swift +44 -0
- package/nitrogen/generated/ios/swift/Role.swift +44 -0
- package/nitrogen/generated/ios/swift/ToolCall.swift +34 -0
- package/nitrogen/generated/ios/swift/ToolDefinition.swift +39 -0
- package/nitrogen/generated/ios/swift/ToolResponse.swift +34 -0
- package/nitrogen/generated/shared/c++/Backend.hpp +80 -0
- package/nitrogen/generated/shared/c++/ExecuteResult.hpp +94 -0
- package/nitrogen/generated/shared/c++/GenerationStats.hpp +103 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +41 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +109 -0
- package/nitrogen/generated/shared/c++/HybridModelStoreSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridModelStoreSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/LLMConfig.hpp +134 -0
- package/nitrogen/generated/shared/c++/MemoryUsage.hpp +95 -0
- package/nitrogen/generated/shared/c++/Message.hpp +89 -0
- package/nitrogen/generated/shared/c++/ModelFile.hpp +95 -0
- package/nitrogen/generated/shared/c++/MultimodalPart.hpp +103 -0
- package/nitrogen/generated/shared/c++/PartType.hpp +80 -0
- package/nitrogen/generated/shared/c++/Role.hpp +80 -0
- package/nitrogen/generated/shared/c++/ToolCall.hpp +87 -0
- package/nitrogen/generated/shared/c++/ToolDefinition.hpp +91 -0
- package/nitrogen/generated/shared/c++/ToolResponse.hpp +87 -0
- package/package.json +115 -0
- package/react-native-litert-lm.podspec +62 -0
- package/react-native.config.js +16 -0
- package/scripts/download-ios-frameworks.sh +39 -0
- package/scripts/framework-source.js +46 -0
- package/scripts/postinstall.js +145 -0
- package/src/__mocks__/react-native-nitro-modules.ts +132 -0
- package/src/__tests__/hooks.test.ts +216 -0
- package/src/__tests__/inferenceRouting.test.ts +36 -0
- package/src/__tests__/memoryTracker.test.ts +87 -0
- package/src/__tests__/modelFactory.test.ts +153 -0
- package/src/__tests__/modelPath.test.ts +14 -0
- package/src/__tests__/modelRegistry.test.ts +73 -0
- package/src/hooks.ts +234 -0
- package/src/index.ts +214 -0
- package/src/inferenceRouting.ts +80 -0
- package/src/memoryTracker.ts +268 -0
- package/src/modelFactory.ts +163 -0
- package/src/modelPath.ts +16 -0
- package/src/modelRegistry.ts +104 -0
- package/src/specs/LiteRTLM.nitro.ts +451 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HybridModelStore.swift
|
|
3
|
+
// react-native-litert-lm
|
|
4
|
+
//
|
|
5
|
+
// Created by Antigravity on 2026-06-04.
|
|
6
|
+
// Copyright © 2026 Margelo. All rights reserved.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
import NitroModules
|
|
11
|
+
|
|
12
|
+
public class HybridModelStore: HybridModelStoreSpec_base, HybridModelStoreSpec_protocol {
|
|
13
|
+
|
|
14
|
+
private let queue = DispatchQueue(label: "dev.litert.modelstore", qos: .utility)
|
|
15
|
+
|
|
16
|
+
private var modelsDirectory: String {
|
|
17
|
+
let cachesDir = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first ?? NSTemporaryDirectory()
|
|
18
|
+
return (cachesDir as NSString).appendingPathComponent("litert_models")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private func sanitizeFileName(_ fileName: String) throws {
|
|
22
|
+
if fileName.contains("..") || fileName.contains("/") || fileName.contains("\\") {
|
|
23
|
+
throw NSError(
|
|
24
|
+
domain: "LiteRTLM.ModelStore",
|
|
25
|
+
code: 400,
|
|
26
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid filename: path traversal or directory separators are not allowed."]
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public func isCached(fileName: String) throws -> Bool {
|
|
32
|
+
try sanitizeFileName(fileName)
|
|
33
|
+
let filePath = (modelsDirectory as NSString).appendingPathComponent(fileName)
|
|
34
|
+
let fileManager = FileManager.default
|
|
35
|
+
if fileManager.fileExists(atPath: filePath) {
|
|
36
|
+
let attrs = try? fileManager.attributesOfItem(atPath: filePath)
|
|
37
|
+
if let fileSize = attrs?[.size] as? UInt64, fileSize > 0 {
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public func getFilePath(fileName: String) throws -> String {
|
|
45
|
+
try sanitizeFileName(fileName)
|
|
46
|
+
return (modelsDirectory as NSString).appendingPathComponent(fileName)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public func listCachedFiles() throws -> [ModelFile] {
|
|
50
|
+
let fileManager = FileManager.default
|
|
51
|
+
let dir = modelsDirectory
|
|
52
|
+
|
|
53
|
+
guard fileManager.fileExists(atPath: dir) else {
|
|
54
|
+
return []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let contents = try fileManager.contentsOfDirectory(atPath: dir)
|
|
58
|
+
var files: [ModelFile] = []
|
|
59
|
+
|
|
60
|
+
for name in contents {
|
|
61
|
+
if name.hasSuffix(".tmp") { continue }
|
|
62
|
+
let path = (dir as NSString).appendingPathComponent(name)
|
|
63
|
+
var isDir: ObjCBool = false
|
|
64
|
+
if fileManager.fileExists(atPath: path, isDirectory: &isDir), !isDir.boolValue {
|
|
65
|
+
let attrs = try? fileManager.attributesOfItem(atPath: path)
|
|
66
|
+
let size = (attrs?[.size] as? UInt64) ?? 0
|
|
67
|
+
let modDate = (attrs?[.modificationDate] as? Date) ?? Date()
|
|
68
|
+
|
|
69
|
+
files.append(ModelFile(
|
|
70
|
+
fileName: name,
|
|
71
|
+
absolutePath: path,
|
|
72
|
+
sizeBytes: Double(size),
|
|
73
|
+
lastModifiedMs: modDate.timeIntervalSince1970 * 1000.0
|
|
74
|
+
))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return files
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public func deleteFile(fileName: String) throws {
|
|
81
|
+
try sanitizeFileName(fileName)
|
|
82
|
+
let filePath = (modelsDirectory as NSString).appendingPathComponent(fileName)
|
|
83
|
+
let fileManager = FileManager.default
|
|
84
|
+
if fileManager.fileExists(atPath: filePath) {
|
|
85
|
+
try fileManager.removeItem(atPath: filePath)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public func downloadFile(
|
|
90
|
+
url: String,
|
|
91
|
+
fileName: String,
|
|
92
|
+
headersJson: String,
|
|
93
|
+
onProgress: @escaping (_ progress: Double) -> Void
|
|
94
|
+
) throws -> Promise<String> {
|
|
95
|
+
let promise = Promise<String>()
|
|
96
|
+
|
|
97
|
+
queue.async {
|
|
98
|
+
do {
|
|
99
|
+
try self.sanitizeFileName(fileName)
|
|
100
|
+
let destPath = (self.modelsDirectory as NSString).appendingPathComponent(fileName)
|
|
101
|
+
let fileManager = FileManager.default
|
|
102
|
+
|
|
103
|
+
if !fileManager.fileExists(atPath: self.modelsDirectory) {
|
|
104
|
+
try fileManager.createDirectory(atPath: self.modelsDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Fast cache check
|
|
108
|
+
if fileManager.fileExists(atPath: destPath) {
|
|
109
|
+
let attrs = try fileManager.attributesOfItem(atPath: destPath)
|
|
110
|
+
if let fileSize = attrs[.size] as? UInt64, fileSize > 0 {
|
|
111
|
+
onProgress(1.0)
|
|
112
|
+
promise.resolve(withResult: destPath)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
guard let downloadUrl = URL(string: url), downloadUrl.scheme?.lowercased() == "https" else {
|
|
118
|
+
promise.reject(withError: NSError(domain: "LiteRTLM.ModelStore", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid download URL: HTTPS is required for security."]))
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onProgress(0.0)
|
|
123
|
+
|
|
124
|
+
// Parse headers JSON
|
|
125
|
+
var customHeaders: [String: String] = [:]
|
|
126
|
+
if !headersJson.isEmpty {
|
|
127
|
+
if let data = headersJson.data(using: .utf8),
|
|
128
|
+
let parsed = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
|
|
129
|
+
customHeaders = parsed
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let sessionConfig = URLSessionConfiguration.default
|
|
134
|
+
sessionConfig.timeoutIntervalForRequest = 30
|
|
135
|
+
sessionConfig.timeoutIntervalForResource = 3600
|
|
136
|
+
|
|
137
|
+
let session = URLSession(configuration: sessionConfig)
|
|
138
|
+
var progressHandler: NSKeyValueObservation?
|
|
139
|
+
|
|
140
|
+
var request = URLRequest(url: downloadUrl)
|
|
141
|
+
for (key, value) in customHeaders {
|
|
142
|
+
request.addValue(value, forHTTPHeaderField: key)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let tempDestFile = (self.modelsDirectory as NSString).appendingPathComponent("\(fileName).tmp")
|
|
146
|
+
|
|
147
|
+
let task = session.downloadTask(with: request) { location, response, error in
|
|
148
|
+
progressHandler?.invalidate()
|
|
149
|
+
|
|
150
|
+
if let error = error {
|
|
151
|
+
promise.reject(withError: error)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 400 {
|
|
156
|
+
promise.reject(withError: NSError(domain: "LiteRTLM.ModelStore", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "HTTP \(httpResponse.statusCode)"]))
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
guard let location = location else {
|
|
161
|
+
promise.reject(withError: NSError(domain: "LiteRTLM.ModelStore", code: 500, userInfo: [NSLocalizedDescriptionKey: "No download location found."]))
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
do {
|
|
166
|
+
// Rename/move atomically via temp file helper
|
|
167
|
+
if fileManager.fileExists(atPath: tempDestFile) {
|
|
168
|
+
try fileManager.removeItem(atPath: tempDestFile)
|
|
169
|
+
}
|
|
170
|
+
try fileManager.moveItem(at: location, to: URL(fileURLWithPath: tempDestFile))
|
|
171
|
+
|
|
172
|
+
if fileManager.fileExists(atPath: destPath) {
|
|
173
|
+
try fileManager.removeItem(atPath: destPath)
|
|
174
|
+
}
|
|
175
|
+
try fileManager.moveItem(at: URL(fileURLWithPath: tempDestFile), to: URL(fileURLWithPath: destPath))
|
|
176
|
+
|
|
177
|
+
onProgress(1.0)
|
|
178
|
+
promise.resolve(withResult: destPath)
|
|
179
|
+
} catch {
|
|
180
|
+
promise.reject(withError: error)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
var lastUpdate = Date()
|
|
185
|
+
progressHandler = task.observe(\.countOfBytesReceived, options: [.new]) { task, _ in
|
|
186
|
+
let expected = task.countOfBytesExpectedToReceive
|
|
187
|
+
if expected > 0 {
|
|
188
|
+
let now = Date()
|
|
189
|
+
if now.timeIntervalSince(lastUpdate) > 0.1 {
|
|
190
|
+
let progress = Double(task.countOfBytesReceived) / Double(expected)
|
|
191
|
+
onProgress(progress)
|
|
192
|
+
lastUpdate = now
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
task.resume()
|
|
198
|
+
session.finishTasksAndInvalidate()
|
|
199
|
+
} catch {
|
|
200
|
+
promise.reject(withError: error)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return promise
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MultimodalPart+Factories.swift
|
|
3
|
+
// react-native-litert-lm
|
|
4
|
+
//
|
|
5
|
+
// Mirrors src/inferenceRouting.ts — keep shapes in sync for native direct hybrid access.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import NitroModules
|
|
9
|
+
|
|
10
|
+
extension MultimodalPart {
|
|
11
|
+
static func textPart(_ text: String) -> MultimodalPart {
|
|
12
|
+
MultimodalPart(type: .text, text: text, path: nil, imageBuffer: nil, audioBuffer: nil)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static func imagePart(_ path: String) -> MultimodalPart {
|
|
16
|
+
MultimodalPart(type: .image, text: nil, path: path, imageBuffer: nil, audioBuffer: nil)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static func audioPart(_ path: String) -> MultimodalPart {
|
|
20
|
+
MultimodalPart(type: .audio, text: nil, path: path, imageBuffer: nil, audioBuffer: nil)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import LiteRTLM
|
|
3
|
+
|
|
4
|
+
class HybridLiteRTLMTests: XCTestCase {
|
|
5
|
+
var bridge: HybridLiteRTLM!
|
|
6
|
+
|
|
7
|
+
override func setUp() {
|
|
8
|
+
super.setUp()
|
|
9
|
+
bridge = HybridLiteRTLM()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override func tearDown() {
|
|
13
|
+
try? bridge.close()
|
|
14
|
+
bridge = nil
|
|
15
|
+
super.tearDown()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func testPathTraversalRejection() async throws {
|
|
19
|
+
let traversals = ["../../etc/passwd", "/absolute/path/file", "subdir\\..\\file", "..", "../", "..\\"]
|
|
20
|
+
for traversal in traversals {
|
|
21
|
+
do {
|
|
22
|
+
let promise = try bridge.deleteModel(fileName: traversal)
|
|
23
|
+
_ = try await promise.await()
|
|
24
|
+
XCTFail("Should have failed for traversal: \(traversal)")
|
|
25
|
+
} catch {
|
|
26
|
+
let nsError = error as NSError
|
|
27
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
28
|
+
XCTAssertEqual(nsError.code, 400)
|
|
29
|
+
XCTAssertTrue(nsError.localizedDescription.contains("path traversal") || nsError.localizedDescription.contains("directory separators"))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func testNonHTTPSDownloadRejection() async throws {
|
|
35
|
+
do {
|
|
36
|
+
let promise = try bridge.downloadModel(url: "http://insecure-domain.com/model.bin", fileName: "model.bin", onProgress: nil)
|
|
37
|
+
_ = try await promise.await()
|
|
38
|
+
XCTFail("Should have blocked insecure HTTP downloads")
|
|
39
|
+
} catch {
|
|
40
|
+
let nsError = error as NSError
|
|
41
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
42
|
+
XCTAssertEqual(nsError.code, 400)
|
|
43
|
+
XCTAssertTrue(nsError.localizedDescription.contains("HTTPS is required"))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func testMemoryTelemetry() {
|
|
48
|
+
XCTAssertNoThrow(try bridge.getMemoryUsage())
|
|
49
|
+
if let mem = try? bridge.getMemoryUsage() {
|
|
50
|
+
XCTAssertGreaterThanOrEqual(mem.nativeHeapBytes, 0.0)
|
|
51
|
+
XCTAssertGreaterThanOrEqual(mem.residentBytes, 0.0)
|
|
52
|
+
XCTAssertGreaterThanOrEqual(mem.availableMemoryBytes, 0.0)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func testSendMessageAsyncRejectsWithoutModel() async throws {
|
|
57
|
+
do {
|
|
58
|
+
let promise = try bridge.sendMessageAsync(message: "hello") { _, _ in }
|
|
59
|
+
_ = try await promise.await()
|
|
60
|
+
XCTFail("Should have failed without model")
|
|
61
|
+
} catch {
|
|
62
|
+
let nsError = error as NSError
|
|
63
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
64
|
+
XCTAssertEqual(nsError.code, 400)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func testSendMessageWithImageAsyncRejectsWithoutModel() async throws {
|
|
69
|
+
do {
|
|
70
|
+
let promise = try bridge.sendMessageWithImageAsync(message: "hello", imagePath: "/tmp/image.jpg") { _, _ in }
|
|
71
|
+
_ = try await promise.await()
|
|
72
|
+
XCTFail("Should have failed without model")
|
|
73
|
+
} catch {
|
|
74
|
+
let nsError = error as NSError
|
|
75
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
76
|
+
XCTAssertEqual(nsError.code, 400)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func testSendMessageWithAudioAsyncRejectsWithoutModel() async throws {
|
|
81
|
+
do {
|
|
82
|
+
let promise = try bridge.sendMessageWithAudioAsync(message: "hello", audioPath: "/tmp/audio.wav") { _, _ in }
|
|
83
|
+
_ = try await promise.await()
|
|
84
|
+
XCTFail("Should have failed without model")
|
|
85
|
+
} catch {
|
|
86
|
+
let nsError = error as NSError
|
|
87
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
88
|
+
XCTAssertEqual(nsError.code, 400)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func testSendMessageWithImageAsyncRejectsFileNotFound() async throws {
|
|
93
|
+
do {
|
|
94
|
+
let promise = try bridge.sendMessageWithImageAsync(message: "hello", imagePath: "/nonexistent/image.jpg") { _, _ in }
|
|
95
|
+
_ = try await promise.await()
|
|
96
|
+
XCTFail("Should have failed without model")
|
|
97
|
+
} catch {
|
|
98
|
+
let nsError = error as NSError
|
|
99
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func testSendMessageWithAudioAsyncRejectsFileNotFound() async throws {
|
|
104
|
+
do {
|
|
105
|
+
let promise = try bridge.sendMessageWithAudioAsync(message: "hello", audioPath: "/nonexistent/audio.wav") { _, _ in }
|
|
106
|
+
_ = try await promise.await()
|
|
107
|
+
XCTFail("Should have failed without model")
|
|
108
|
+
} catch {
|
|
109
|
+
let nsError = error as NSError
|
|
110
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func testInitialStats() {
|
|
115
|
+
XCTAssertNoThrow(try bridge.getStats())
|
|
116
|
+
if let stats = try? bridge.getStats() {
|
|
117
|
+
XCTAssertEqual(stats.promptTokens, 0.0)
|
|
118
|
+
XCTAssertEqual(stats.completionTokens, 0.0)
|
|
119
|
+
XCTAssertEqual(stats.totalTokens, 0.0)
|
|
120
|
+
XCTAssertEqual(stats.timeToFirstToken, 0.0)
|
|
121
|
+
XCTAssertEqual(stats.totalTime, 0.0)
|
|
122
|
+
XCTAssertEqual(stats.tokensPerSecond, 0.0)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func testDeleteModelCleanupLogic() async throws {
|
|
127
|
+
bridge.loadedModelPath = "/path/to/my_loaded_model.litertlm"
|
|
128
|
+
|
|
129
|
+
let promise1 = try bridge.deleteModel(fileName: "other_model.litertlm")
|
|
130
|
+
_ = try await promise1.await()
|
|
131
|
+
XCTAssertEqual(bridge.loadedModelPath, "/path/to/my_loaded_model.litertlm")
|
|
132
|
+
|
|
133
|
+
let promise2 = try bridge.deleteModel(fileName: "my_loaded_model.litertlm")
|
|
134
|
+
_ = try await promise2.await()
|
|
135
|
+
XCTAssertNil(bridge.loadedModelPath)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func testExecutePathPrecedenceOverBuffer() async throws {
|
|
139
|
+
let pathPart = MultimodalPart(
|
|
140
|
+
type: .image,
|
|
141
|
+
text: nil,
|
|
142
|
+
path: "/nonexistent/precedence_test_image.jpg",
|
|
143
|
+
imageBuffer: nil,
|
|
144
|
+
audioBuffer: nil
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
do {
|
|
148
|
+
let promise = try bridge.execute(parts: [pathPart], onToken: nil)
|
|
149
|
+
_ = try await promise.await()
|
|
150
|
+
XCTFail("Should have failed")
|
|
151
|
+
} catch {
|
|
152
|
+
let nsError = error as NSError
|
|
153
|
+
XCTAssertTrue(nsError.localizedDescription.contains("file not found: /nonexistent/precedence_test_image.jpg"))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func testExecuteTempFileCleanupOnError() async throws {
|
|
158
|
+
let dummyData = Data([0, 1, 2, 3])
|
|
159
|
+
let buffer = ArrayBuffer(dummyData)
|
|
160
|
+
|
|
161
|
+
let bufferPart = MultimodalPart(
|
|
162
|
+
type: .image,
|
|
163
|
+
text: nil,
|
|
164
|
+
path: nil,
|
|
165
|
+
imageBuffer: buffer,
|
|
166
|
+
audioBuffer: nil
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
let invalidPathPart = MultimodalPart(
|
|
170
|
+
type: .image,
|
|
171
|
+
text: nil,
|
|
172
|
+
path: "/nonexistent/invalid_file_cleanup_test.jpg",
|
|
173
|
+
imageBuffer: nil,
|
|
174
|
+
audioBuffer: nil
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
do {
|
|
178
|
+
let promise = try bridge.execute(parts: [bufferPart, invalidPathPart], onToken: nil)
|
|
179
|
+
_ = try await promise.await()
|
|
180
|
+
XCTFail("Should have failed")
|
|
181
|
+
} catch {
|
|
182
|
+
let tempDirAfter = try FileManager.default.contentsOfDirectory(atPath: NSTemporaryDirectory())
|
|
183
|
+
let leakedFiles = tempDirAfter.filter { $0.contains("litert_buf_") }
|
|
184
|
+
XCTAssertEqual(leakedFiles.count, 0)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { MultimodalPart } from "../specs/LiteRTLM.nitro";
|
|
2
|
+
export declare const mockLiteRTLM: {
|
|
3
|
+
isReady: jest.Mock<boolean, [], any>;
|
|
4
|
+
loadModel: jest.Mock<any, any, any>;
|
|
5
|
+
execute: jest.Mock<Promise<string>, [parts: MultimodalPart[], onToken?: ((token: string, done: boolean) => void) | undefined], any>;
|
|
6
|
+
sendMessage: jest.Mock<Promise<string>, [message: string], any>;
|
|
7
|
+
sendMessageWithImage: jest.Mock<Promise<string>, [message: string, imagePath: string], any>;
|
|
8
|
+
downloadModel: jest.Mock<Promise<string>, [url: string, fileName: string, onProgress?: ((progress: number) => void) | undefined], any>;
|
|
9
|
+
deleteModel: jest.Mock<any, any, any>;
|
|
10
|
+
sendMessageWithAudio: jest.Mock<Promise<string>, [message: string, audioPath: string], any>;
|
|
11
|
+
sendMultimodalMessage: jest.Mock<Promise<string>, [parts: MultimodalPart[]], any>;
|
|
12
|
+
sendMessageAsync: jest.Mock<Promise<void>, [msg: string, onToken: (token: string, done: boolean) => void], any>;
|
|
13
|
+
sendMessageWithImageAsync: jest.Mock<Promise<void>, [msg: string, imagePath: string, onToken: (token: string, done: boolean) => void], any>;
|
|
14
|
+
sendMessageWithAudioAsync: jest.Mock<Promise<void>, [msg: string, audioPath: string, onToken: (token: string, done: boolean) => void], any>;
|
|
15
|
+
getHistory: jest.Mock<never[], [], any>;
|
|
16
|
+
resetConversation: jest.Mock<any, any, any>;
|
|
17
|
+
getStats: jest.Mock<{
|
|
18
|
+
promptTokens: number;
|
|
19
|
+
completionTokens: number;
|
|
20
|
+
totalTokens: number;
|
|
21
|
+
timeToFirstToken: number;
|
|
22
|
+
totalTime: number;
|
|
23
|
+
tokensPerSecond: number;
|
|
24
|
+
}, [], any>;
|
|
25
|
+
countTokens: jest.Mock<number, [], any>;
|
|
26
|
+
getMemoryUsage: jest.Mock<{
|
|
27
|
+
nativeHeapBytes: number;
|
|
28
|
+
residentBytes: number;
|
|
29
|
+
availableMemoryBytes: number;
|
|
30
|
+
isLowMemory: boolean;
|
|
31
|
+
}, [], any>;
|
|
32
|
+
close: jest.Mock<any, any, any>;
|
|
33
|
+
};
|
|
34
|
+
export declare const mockModelStore: {
|
|
35
|
+
isCached: jest.Mock<boolean, [fileName: string], any>;
|
|
36
|
+
getFilePath: jest.Mock<string, [fileName: string], any>;
|
|
37
|
+
listCachedFiles: jest.Mock<never[], [], any>;
|
|
38
|
+
deleteFile: jest.Mock<any, [fileName: string], any>;
|
|
39
|
+
downloadFile: jest.Mock<Promise<string>, [url: string, fileName: string, headersJson: string, onProgress: (progress: number) => void], any>;
|
|
40
|
+
};
|
|
41
|
+
export declare const NitroModules: {
|
|
42
|
+
createHybridObject: jest.Mock<{
|
|
43
|
+
isReady: jest.Mock<boolean, [], any>;
|
|
44
|
+
loadModel: jest.Mock<any, any, any>;
|
|
45
|
+
execute: jest.Mock<Promise<string>, [parts: MultimodalPart[], onToken?: ((token: string, done: boolean) => void) | undefined], any>;
|
|
46
|
+
sendMessage: jest.Mock<Promise<string>, [message: string], any>;
|
|
47
|
+
sendMessageWithImage: jest.Mock<Promise<string>, [message: string, imagePath: string], any>;
|
|
48
|
+
downloadModel: jest.Mock<Promise<string>, [url: string, fileName: string, onProgress?: ((progress: number) => void) | undefined], any>;
|
|
49
|
+
deleteModel: jest.Mock<any, any, any>;
|
|
50
|
+
sendMessageWithAudio: jest.Mock<Promise<string>, [message: string, audioPath: string], any>;
|
|
51
|
+
sendMultimodalMessage: jest.Mock<Promise<string>, [parts: MultimodalPart[]], any>;
|
|
52
|
+
sendMessageAsync: jest.Mock<Promise<void>, [msg: string, onToken: (token: string, done: boolean) => void], any>;
|
|
53
|
+
sendMessageWithImageAsync: jest.Mock<Promise<void>, [msg: string, imagePath: string, onToken: (token: string, done: boolean) => void], any>;
|
|
54
|
+
sendMessageWithAudioAsync: jest.Mock<Promise<void>, [msg: string, audioPath: string, onToken: (token: string, done: boolean) => void], any>;
|
|
55
|
+
getHistory: jest.Mock<never[], [], any>;
|
|
56
|
+
resetConversation: jest.Mock<any, any, any>;
|
|
57
|
+
getStats: jest.Mock<{
|
|
58
|
+
promptTokens: number;
|
|
59
|
+
completionTokens: number;
|
|
60
|
+
totalTokens: number;
|
|
61
|
+
timeToFirstToken: number;
|
|
62
|
+
totalTime: number;
|
|
63
|
+
tokensPerSecond: number;
|
|
64
|
+
}, [], any>;
|
|
65
|
+
countTokens: jest.Mock<number, [], any>;
|
|
66
|
+
getMemoryUsage: jest.Mock<{
|
|
67
|
+
nativeHeapBytes: number;
|
|
68
|
+
residentBytes: number;
|
|
69
|
+
availableMemoryBytes: number;
|
|
70
|
+
isLowMemory: boolean;
|
|
71
|
+
}, [], any>;
|
|
72
|
+
close: jest.Mock<any, any, any>;
|
|
73
|
+
} | {
|
|
74
|
+
isCached: jest.Mock<boolean, [fileName: string], any>;
|
|
75
|
+
getFilePath: jest.Mock<string, [fileName: string], any>;
|
|
76
|
+
listCachedFiles: jest.Mock<never[], [], any>;
|
|
77
|
+
deleteFile: jest.Mock<any, [fileName: string], any>;
|
|
78
|
+
downloadFile: jest.Mock<Promise<string>, [url: string, fileName: string, headersJson: string, onProgress: (progress: number) => void], any>;
|
|
79
|
+
}, [name: string], any>;
|
|
80
|
+
createNativeArrayBuffer: jest.Mock<ArrayBuffer, [size: number], any>;
|
|
81
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NitroModules = exports.mockModelStore = exports.mockLiteRTLM = void 0;
|
|
4
|
+
function streamMockTokens(parts, onToken) {
|
|
5
|
+
const hasImage = parts.some((p) => p.type === "image");
|
|
6
|
+
const hasAudio = parts.some((p) => p.type === "audio");
|
|
7
|
+
if (hasImage) {
|
|
8
|
+
onToken("Mock vision ", false);
|
|
9
|
+
onToken("token", true);
|
|
10
|
+
}
|
|
11
|
+
else if (hasAudio) {
|
|
12
|
+
onToken("Mock audio ", false);
|
|
13
|
+
onToken("token", true);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
onToken("Mock ", false);
|
|
17
|
+
onToken("token", true);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function mockExecuteResponse(parts) {
|
|
21
|
+
const hasImage = parts.some((p) => p.type === "image");
|
|
22
|
+
const hasAudio = parts.some((p) => p.type === "audio");
|
|
23
|
+
return hasImage
|
|
24
|
+
? "Mock vision token"
|
|
25
|
+
: hasAudio
|
|
26
|
+
? "Mock audio token"
|
|
27
|
+
: "Mock token";
|
|
28
|
+
}
|
|
29
|
+
const mockExecute = jest.fn((parts, onToken) => {
|
|
30
|
+
if (onToken) {
|
|
31
|
+
streamMockTokens(parts, onToken);
|
|
32
|
+
}
|
|
33
|
+
return Promise.resolve(mockExecuteResponse(parts));
|
|
34
|
+
});
|
|
35
|
+
exports.mockLiteRTLM = {
|
|
36
|
+
isReady: jest.fn(() => false),
|
|
37
|
+
loadModel: jest.fn().mockResolvedValue(undefined),
|
|
38
|
+
execute: mockExecute,
|
|
39
|
+
sendMessage: jest.fn((message) => mockExecute([{ type: "text", text: message }])),
|
|
40
|
+
sendMessageWithImage: jest.fn((message, imagePath) => mockExecute([
|
|
41
|
+
{ type: "text", text: message },
|
|
42
|
+
{ type: "image", path: imagePath },
|
|
43
|
+
])),
|
|
44
|
+
downloadModel: jest.fn(async (url, fileName, onProgress) => {
|
|
45
|
+
onProgress?.(1.0);
|
|
46
|
+
return "/mock/path/model.litertlm";
|
|
47
|
+
}),
|
|
48
|
+
deleteModel: jest.fn().mockResolvedValue(undefined),
|
|
49
|
+
sendMessageWithAudio: jest.fn((message, audioPath) => mockExecute([
|
|
50
|
+
{ type: "text", text: message },
|
|
51
|
+
{ type: "audio", path: audioPath },
|
|
52
|
+
])),
|
|
53
|
+
sendMultimodalMessage: jest.fn((parts) => mockExecute(parts)),
|
|
54
|
+
sendMessageAsync: jest.fn((msg, onToken) => mockExecute([{ type: "text", text: msg }], onToken).then(() => { })),
|
|
55
|
+
sendMessageWithImageAsync: jest.fn((msg, imagePath, onToken) => mockExecute([{ type: "text", text: msg }, { type: "image", path: imagePath }], onToken).then(() => { })),
|
|
56
|
+
sendMessageWithAudioAsync: jest.fn((msg, audioPath, onToken) => mockExecute([{ type: "text", text: msg }, { type: "audio", path: audioPath }], onToken).then(() => { })),
|
|
57
|
+
getHistory: jest.fn(() => []),
|
|
58
|
+
resetConversation: jest.fn(),
|
|
59
|
+
getStats: jest.fn(() => ({
|
|
60
|
+
promptTokens: 10,
|
|
61
|
+
completionTokens: 20,
|
|
62
|
+
totalTokens: 30,
|
|
63
|
+
timeToFirstToken: 5,
|
|
64
|
+
totalTime: 50,
|
|
65
|
+
tokensPerSecond: 400,
|
|
66
|
+
})),
|
|
67
|
+
countTokens: jest.fn(() => -1),
|
|
68
|
+
getMemoryUsage: jest.fn(() => ({
|
|
69
|
+
nativeHeapBytes: 1000000,
|
|
70
|
+
residentBytes: 2000000,
|
|
71
|
+
availableMemoryBytes: 4000000,
|
|
72
|
+
isLowMemory: false,
|
|
73
|
+
})),
|
|
74
|
+
close: jest.fn(),
|
|
75
|
+
};
|
|
76
|
+
exports.mockModelStore = {
|
|
77
|
+
isCached: jest.fn((fileName) => false),
|
|
78
|
+
getFilePath: jest.fn((fileName) => `/mock/path/${fileName}`),
|
|
79
|
+
listCachedFiles: jest.fn(() => []),
|
|
80
|
+
deleteFile: jest.fn((fileName) => {
|
|
81
|
+
return exports.mockLiteRTLM.deleteModel(fileName);
|
|
82
|
+
}),
|
|
83
|
+
downloadFile: jest.fn(async (url, fileName, headersJson, onProgress) => exports.mockLiteRTLM.downloadModel(url, fileName, onProgress)),
|
|
84
|
+
};
|
|
85
|
+
exports.NitroModules = {
|
|
86
|
+
createHybridObject: jest.fn((name) => {
|
|
87
|
+
if (name === "LiteRTLM") {
|
|
88
|
+
return exports.mockLiteRTLM;
|
|
89
|
+
}
|
|
90
|
+
if (name === "ModelStore") {
|
|
91
|
+
return exports.mockModelStore;
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Mock not implemented for hybrid object: ${name}`);
|
|
94
|
+
}),
|
|
95
|
+
createNativeArrayBuffer: jest.fn((size) => {
|
|
96
|
+
return new ArrayBuffer(size);
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|