@fireflydb/expo-driver 0.0.6
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/README.md +253 -0
- package/android/build.gradle +51 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +21 -0
- package/android/src/main/cpp/firefly_jni.cpp +110 -0
- package/android/src/main/jniLibs/arm64-v8a/libfirefly.so +0 -0
- package/android/src/main/jniLibs/armeabi-v7a/libfirefly.so +0 -0
- package/android/src/main/jniLibs/x86/libfirefly.so +0 -0
- package/android/src/main/jniLibs/x86_64/libfirefly.so +0 -0
- package/android/src/main/kotlin/com/fireflydb/expo/FireflyClientModule.kt +101 -0
- package/android/src/main/kotlin/com/fireflydb/expo/FireflyJniBridge.kt +58 -0
- package/expo-module.config.json +9 -0
- package/ios/Firefly.xcframework/Info.plist +44 -0
- package/ios/Firefly.xcframework/ios-arm64/Firefly.framework/Firefly +0 -0
- package/ios/Firefly.xcframework/ios-arm64/Firefly.framework/Info.plist +30 -0
- package/ios/Firefly.xcframework/ios-arm64_x86_64-simulator/Firefly.framework/Firefly +0 -0
- package/ios/Firefly.xcframework/ios-arm64_x86_64-simulator/Firefly.framework/Info.plist +30 -0
- package/ios/FireflyClient.podspec +33 -0
- package/ios/FireflyClientModule.swift +193 -0
- package/package.json +50 -0
- package/scripts/build-libfirefly.sh +224 -0
- package/src/FireflyClientModule.ts +83 -0
- package/src/drivers/secureStorage.ts +83 -0
- package/src/drivers/sqlite.ts +322 -0
- package/src/drivers/websocket.ts +166 -0
- package/src/index.ts +120 -0
- package/src/polyfill.ts +22 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>en</string>
|
|
7
|
+
<key>CFBundleExecutable</key>
|
|
8
|
+
<string>Firefly</string>
|
|
9
|
+
<key>CFBundleIdentifier</key>
|
|
10
|
+
<string>com.fireflydb.firefly</string>
|
|
11
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
12
|
+
<string>6.0</string>
|
|
13
|
+
<key>CFBundleName</key>
|
|
14
|
+
<string>Firefly</string>
|
|
15
|
+
<key>CFBundlePackageType</key>
|
|
16
|
+
<string>FMWK</string>
|
|
17
|
+
<key>CFBundleShortVersionString</key>
|
|
18
|
+
<string>1.0</string>
|
|
19
|
+
<key>CFBundleVersion</key>
|
|
20
|
+
<string>1</string>
|
|
21
|
+
<key>CFBundleSupportedPlatforms</key>
|
|
22
|
+
<array>
|
|
23
|
+
<string>iPhoneOS</string>
|
|
24
|
+
</array>
|
|
25
|
+
<key>DTPlatformName</key>
|
|
26
|
+
<string>iphoneos</string>
|
|
27
|
+
<key>MinimumOSVersion</key>
|
|
28
|
+
<string>15.1</string>
|
|
29
|
+
</dict>
|
|
30
|
+
</plist>
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>en</string>
|
|
7
|
+
<key>CFBundleExecutable</key>
|
|
8
|
+
<string>Firefly</string>
|
|
9
|
+
<key>CFBundleIdentifier</key>
|
|
10
|
+
<string>com.fireflydb.firefly</string>
|
|
11
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
12
|
+
<string>6.0</string>
|
|
13
|
+
<key>CFBundleName</key>
|
|
14
|
+
<string>Firefly</string>
|
|
15
|
+
<key>CFBundlePackageType</key>
|
|
16
|
+
<string>FMWK</string>
|
|
17
|
+
<key>CFBundleShortVersionString</key>
|
|
18
|
+
<string>1.0</string>
|
|
19
|
+
<key>CFBundleVersion</key>
|
|
20
|
+
<string>1</string>
|
|
21
|
+
<key>CFBundleSupportedPlatforms</key>
|
|
22
|
+
<array>
|
|
23
|
+
<string>iPhoneSimulator</string>
|
|
24
|
+
</array>
|
|
25
|
+
<key>DTPlatformName</key>
|
|
26
|
+
<string>iphonesimulator</string>
|
|
27
|
+
<key>MinimumOSVersion</key>
|
|
28
|
+
<string>15.1</string>
|
|
29
|
+
</dict>
|
|
30
|
+
</plist>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'FireflyClient'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = 'MIT'
|
|
10
|
+
s.author = { 'FireflyDB' => 'noreply@example.com' }
|
|
11
|
+
s.homepage = 'https://github.com/fireflydb/fireflydb'
|
|
12
|
+
s.platforms = { :ios => '15.1' }
|
|
13
|
+
s.swift_version = '5.9'
|
|
14
|
+
s.source = { :git => 'https://github.com/fireflydb/fireflydb.git' }
|
|
15
|
+
|
|
16
|
+
s.dependency 'ExpoModulesCore'
|
|
17
|
+
s.dependency 'ExpoSQLite'
|
|
18
|
+
|
|
19
|
+
s.source_files = ['*.swift', '_shared/*.{cpp,h}']
|
|
20
|
+
|
|
21
|
+
# Firefly.xcframework wraps a proper Firefly.framework for each iOS slice
|
|
22
|
+
# (device-arm64 and simulator-universal). CocoaPods + Xcode embed the right
|
|
23
|
+
# framework into <App>.app/Frameworks/Firefly.framework/Firefly and re-sign
|
|
24
|
+
# at archive time. See packages/expo/scripts/build-libfirefly.sh.
|
|
25
|
+
s.vendored_frameworks = 'Firefly.xcframework'
|
|
26
|
+
|
|
27
|
+
s.pod_target_xcconfig = {
|
|
28
|
+
'DEFINES_MODULE' => 'YES',
|
|
29
|
+
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17',
|
|
30
|
+
'CLANG_CXX_LIBRARY' => 'libc++',
|
|
31
|
+
'OTHER_CPLUSPLUSFLAGS' => '-fvisibility=hidden'
|
|
32
|
+
}
|
|
33
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
// MARK: - Shared C++ listener registry bindings
|
|
5
|
+
//
|
|
6
|
+
// Listener registration goes through the cross-platform registry in
|
|
7
|
+
// `cpp/firefly_listener.{h,cpp}` (linked into this pod via the _shared
|
|
8
|
+
// symlink configured in FireflyClient.podspec). The registry owns the
|
|
9
|
+
// libfirefly C-ABI lifetime so the iOS side only has to forward blobs
|
|
10
|
+
// to JS.
|
|
11
|
+
//
|
|
12
|
+
// We use an `Int64` handle as the stable identifier between Swift and
|
|
13
|
+
// the C++ registry. The dispatcher receives that handle on every fire
|
|
14
|
+
// and looks up the live module reference in `swiftRegistry` below; a
|
|
15
|
+
// lookup miss after `removeChangeListener` is the documented "stale
|
|
16
|
+
// fire" path and a silent no-op — this is how we honor the libfirefly
|
|
17
|
+
// contract requirement that user_data outlive
|
|
18
|
+
// `firefly_remove_change_listener` calls without freeing any heap
|
|
19
|
+
// object the C-side might dereference.
|
|
20
|
+
|
|
21
|
+
private typealias FireflyDispatchFn = @convention(c) (
|
|
22
|
+
Int64, UnsafePointer<UInt8>?, Int
|
|
23
|
+
) -> Void
|
|
24
|
+
|
|
25
|
+
@_silgen_name("firefly_listener_register")
|
|
26
|
+
private func firefly_listener_register(
|
|
27
|
+
_ path: UnsafePointer<CChar>?,
|
|
28
|
+
_ handle: Int64,
|
|
29
|
+
_ dispatch: FireflyDispatchFn?
|
|
30
|
+
) -> UInt64
|
|
31
|
+
|
|
32
|
+
@_silgen_name("firefly_listener_unregister")
|
|
33
|
+
private func firefly_listener_unregister(_ handle: Int64)
|
|
34
|
+
|
|
35
|
+
// MARK: - Swift-side dispatch table
|
|
36
|
+
//
|
|
37
|
+
// The C++ thunk hands us only the integer handle. We keep a static
|
|
38
|
+
// `[Int64: WeakModule]` so a callback can find the right Expo module
|
|
39
|
+
// instance to fire `sendEvent` on. The map entry is just a weak ref +
|
|
40
|
+
// bookkeeping — there's no heap object whose deallocation could race
|
|
41
|
+
// with an in-flight fire. Hot-reload re-instantiates the module: the
|
|
42
|
+
// new instance registers fresh handles and the old (gone) entries
|
|
43
|
+
// drain out as the C++ registry unregisters them on `OnDestroy`.
|
|
44
|
+
|
|
45
|
+
private final class WeakModule {
|
|
46
|
+
weak var module: FireflyClientModule?
|
|
47
|
+
init(_ module: FireflyClientModule) { self.module = module }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private let swiftRegistryLock = NSLock()
|
|
51
|
+
private var swiftRegistry: [Int64: WeakModule] = [:]
|
|
52
|
+
|
|
53
|
+
private let fireflyDispatch: FireflyDispatchFn = { handle, blobPtr, blobLen in
|
|
54
|
+
guard let blobPtr = blobPtr else { return }
|
|
55
|
+
|
|
56
|
+
swiftRegistryLock.lock()
|
|
57
|
+
let weakBox = swiftRegistry[handle]
|
|
58
|
+
swiftRegistryLock.unlock()
|
|
59
|
+
|
|
60
|
+
guard let module = weakBox?.module else { return }
|
|
61
|
+
|
|
62
|
+
// Copy bytes — the buffer is only valid for the duration of this call.
|
|
63
|
+
let blob = Data(bytes: blobPtr, count: blobLen)
|
|
64
|
+
// sendEvent is safe to call from any thread; ExpoModulesCore marshals
|
|
65
|
+
// onto the JS thread internally.
|
|
66
|
+
module.sendEvent("FireflyChange", [
|
|
67
|
+
"id": NSNumber(value: handle),
|
|
68
|
+
"blob": blob,
|
|
69
|
+
])
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private let nextHandleLock = NSLock()
|
|
73
|
+
private var nextHandleCounter: Int64 = 1
|
|
74
|
+
private func mintHandle() -> Int64 {
|
|
75
|
+
nextHandleLock.lock()
|
|
76
|
+
defer { nextHandleLock.unlock() }
|
|
77
|
+
let h = nextHandleCounter
|
|
78
|
+
nextHandleCounter += 1
|
|
79
|
+
return h
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public class FireflyClientModule: Module {
|
|
83
|
+
// Set of handles registered against this module instance. Used on
|
|
84
|
+
// teardown to unregister everything still in flight.
|
|
85
|
+
private var liveHandles: Set<Int64> = []
|
|
86
|
+
private let liveHandlesLock = NSLock()
|
|
87
|
+
|
|
88
|
+
public func definition() -> ModuleDefinition {
|
|
89
|
+
Name("FireflyClient")
|
|
90
|
+
|
|
91
|
+
Events("FireflyChange")
|
|
92
|
+
|
|
93
|
+
Function("getLibraryPath") { () -> String in
|
|
94
|
+
// Firefly.framework is embedded by Xcode into <App>.app/Frameworks/
|
|
95
|
+
// Firefly.framework/Firefly (the binary has no extension since that's
|
|
96
|
+
// the standard framework layout). We return the path to that binary.
|
|
97
|
+
//
|
|
98
|
+
// SQLite's sqlite3_load_extension auto-appends ".dylib" when the file
|
|
99
|
+
// at the given path doesn't exist; since the framework binary DOES
|
|
100
|
+
// exist at this exact path, dlopen opens it directly without any
|
|
101
|
+
// suffix mangling.
|
|
102
|
+
let frameworkBinary = Bundle.main.bundleURL
|
|
103
|
+
.appendingPathComponent("Frameworks/Firefly.framework/Firefly")
|
|
104
|
+
.path
|
|
105
|
+
if FileManager.default.fileExists(atPath: frameworkBinary) {
|
|
106
|
+
return frameworkBinary
|
|
107
|
+
}
|
|
108
|
+
throw NSError(
|
|
109
|
+
domain: "FireflyClient",
|
|
110
|
+
code: 1,
|
|
111
|
+
userInfo: [NSLocalizedDescriptionKey: "Firefly.framework not found at \(frameworkBinary). Did the host app rebuild after pod install?"]
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Function("getEntryPoint") { () -> String in
|
|
116
|
+
return "sqlite3_firefly_init"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Constants([
|
|
120
|
+
"entryPoint": "sqlite3_firefly_init"
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
// Register a change listener against the SQLite database file at
|
|
124
|
+
// `path`. The shared C++ registry tracks the rust-side listener id;
|
|
125
|
+
// we just hand it a freshly-minted handle and a weak ref to this
|
|
126
|
+
// module. Throws if the registry rejects (in-memory DB, bad path).
|
|
127
|
+
Function("addChangeListener") { (path: String) -> Double in
|
|
128
|
+
let handle = mintHandle()
|
|
129
|
+
|
|
130
|
+
swiftRegistryLock.lock()
|
|
131
|
+
swiftRegistry[handle] = WeakModule(self)
|
|
132
|
+
swiftRegistryLock.unlock()
|
|
133
|
+
|
|
134
|
+
let rustId: UInt64 = path.withCString { cstr in
|
|
135
|
+
firefly_listener_register(cstr, handle, fireflyDispatch)
|
|
136
|
+
}
|
|
137
|
+
if rustId == 0 {
|
|
138
|
+
swiftRegistryLock.lock()
|
|
139
|
+
swiftRegistry.removeValue(forKey: handle)
|
|
140
|
+
swiftRegistryLock.unlock()
|
|
141
|
+
throw NSError(
|
|
142
|
+
domain: "FireflyClient",
|
|
143
|
+
code: 2,
|
|
144
|
+
userInfo: [NSLocalizedDescriptionKey: "firefly_listener_register rejected path '\(path)' (in-memory DB or invalid path?)"]
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
self.liveHandlesLock.lock()
|
|
149
|
+
self.liveHandles.insert(handle)
|
|
150
|
+
self.liveHandlesLock.unlock()
|
|
151
|
+
|
|
152
|
+
// Return as Double so JS gets a regular Number. handle stays well
|
|
153
|
+
// below 2^53 in any realistic session lifespan.
|
|
154
|
+
return Double(handle)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
Function("removeChangeListener") { (handleDouble: Double) -> Void in
|
|
158
|
+
let handle = Int64(handleDouble)
|
|
159
|
+
|
|
160
|
+
self.liveHandlesLock.lock()
|
|
161
|
+
let wasLive = self.liveHandles.remove(handle) != nil
|
|
162
|
+
self.liveHandlesLock.unlock()
|
|
163
|
+
guard wasLive else { return }
|
|
164
|
+
|
|
165
|
+
// Tell the C++ registry to drop the entry first; this also calls
|
|
166
|
+
// `firefly_remove_change_listener`. Any in-flight fire that
|
|
167
|
+
// arrived before we erased will dispatch with a stale module
|
|
168
|
+
// weak-ref check; arrivals after the erase silently no-op via
|
|
169
|
+
// lookup miss in the C++ registry.
|
|
170
|
+
firefly_listener_unregister(handle)
|
|
171
|
+
|
|
172
|
+
swiftRegistryLock.lock()
|
|
173
|
+
swiftRegistry.removeValue(forKey: handle)
|
|
174
|
+
swiftRegistryLock.unlock()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
OnDestroy {
|
|
178
|
+
// Best-effort cleanup if the module is being torn down (e.g. during
|
|
179
|
+
// hot reload). Removes every still-registered listener.
|
|
180
|
+
self.liveHandlesLock.lock()
|
|
181
|
+
let pending = self.liveHandles
|
|
182
|
+
self.liveHandles.removeAll()
|
|
183
|
+
self.liveHandlesLock.unlock()
|
|
184
|
+
|
|
185
|
+
for handle in pending {
|
|
186
|
+
firefly_listener_unregister(handle)
|
|
187
|
+
swiftRegistryLock.lock()
|
|
188
|
+
swiftRegistry.removeValue(forKey: handle)
|
|
189
|
+
swiftRegistryLock.unlock()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fireflydb/expo-driver",
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "Expo native module + drivers for FireflyDB. Bundles libfirefly and wires it through expo-sqlite, expo-secure-store, and React Native WebSocket.",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"ios",
|
|
10
|
+
"android/build.gradle",
|
|
11
|
+
"android/src",
|
|
12
|
+
"expo-module.config.json",
|
|
13
|
+
"scripts/build-libfirefly.sh"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@fireflydb/core": "0.0.6"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"expo": ">=53.0.0",
|
|
20
|
+
"expo-crypto": ">=14.0.0",
|
|
21
|
+
"expo-modules-core": ">=2.0.0",
|
|
22
|
+
"expo-secure-store": ">=14.0.0",
|
|
23
|
+
"expo-sqlite": ">=15.0.0",
|
|
24
|
+
"react-native": ">=0.76.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"expo": "^55.0.0",
|
|
28
|
+
"expo-crypto": "^55.0.0",
|
|
29
|
+
"expo-modules-core": "^55.0.0",
|
|
30
|
+
"expo-secure-store": "^55.0.0",
|
|
31
|
+
"expo-sqlite": "^55.0.0",
|
|
32
|
+
"react": "^19.2.0",
|
|
33
|
+
"react-native": "^0.83.6",
|
|
34
|
+
"typescript": "^5.5.0",
|
|
35
|
+
"vitest": "^2.1.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"expo",
|
|
39
|
+
"expo-module",
|
|
40
|
+
"sqlite",
|
|
41
|
+
"crdt",
|
|
42
|
+
"fireflydb"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build:libfirefly": "bash scripts/build-libfirefly.sh",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"test": "vitest run --passWithNoTests"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Cross-compile libfirefly for iOS (fat dylib) and Android (jniLibs).
|
|
4
|
+
#
|
|
5
|
+
# Outputs:
|
|
6
|
+
# ios/libfirefly.dylib (fat: arm64 device + arm64-sim + x86_64-sim)
|
|
7
|
+
# android/src/main/jniLibs/{arm64-v8a,armeabi-v7a,x86_64,x86}/libfirefly.so
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash scripts/build-libfirefly.sh # both platforms
|
|
11
|
+
# bash scripts/build-libfirefly.sh ios # iOS only
|
|
12
|
+
# bash scripts/build-libfirefly.sh android # Android only
|
|
13
|
+
#
|
|
14
|
+
# Requires:
|
|
15
|
+
# - rustup with iOS targets (auto-installed if missing)
|
|
16
|
+
# - Xcode + xcodebuild + lipo (for iOS)
|
|
17
|
+
# - cargo-ndk + Android NDK r27+ (for Android, auto-installed if missing)
|
|
18
|
+
#
|
|
19
|
+
# Source recipes ported from:
|
|
20
|
+
# /Users/maxencehenneron/Documents/Projects/Appsent/Firefly/scripts/build-ios.sh
|
|
21
|
+
# /Users/maxencehenneron/Documents/Projects/Appsent/Firefly/Makefile (ios-* / android-* targets)
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
|
|
25
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
PKG_DIR="$(dirname "$SCRIPT_DIR")"
|
|
27
|
+
SDK_DIR="$(cd "$PKG_DIR/../.." && pwd)"
|
|
28
|
+
REPO_ROOT="$(cd "$SDK_DIR/../.." && pwd)"
|
|
29
|
+
# core/ is part of a Rust workspace at REPO_ROOT, so cargo emits to
|
|
30
|
+
# $REPO_ROOT/target, not $REPO_ROOT/core/target.
|
|
31
|
+
CORE_DIR="$REPO_ROOT/core"
|
|
32
|
+
CARGO_TARGET_DIR="$REPO_ROOT/target"
|
|
33
|
+
|
|
34
|
+
IOS_XCF_OUT="$PKG_DIR/ios/Firefly.xcframework"
|
|
35
|
+
IOS_FRAMEWORK_NAME="Firefly"
|
|
36
|
+
IOS_BUNDLE_ID="com.fireflydb.firefly"
|
|
37
|
+
IOS_DEPLOYMENT_TARGET="15.1"
|
|
38
|
+
ANDROID_JNI_OUT="$PKG_DIR/android/src/main/jniLibs"
|
|
39
|
+
|
|
40
|
+
IOS_TARGETS=(
|
|
41
|
+
"aarch64-apple-ios"
|
|
42
|
+
"aarch64-apple-ios-sim"
|
|
43
|
+
"x86_64-apple-ios"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# (rust-target, android-abi)
|
|
47
|
+
ANDROID_TARGETS=(
|
|
48
|
+
"aarch64-linux-android arm64-v8a"
|
|
49
|
+
"armv7-linux-androideabi armeabi-v7a"
|
|
50
|
+
"x86_64-linux-android x86_64"
|
|
51
|
+
"i686-linux-android x86"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
CARGO_FEATURES="--no-default-features --features loadable_extension"
|
|
55
|
+
|
|
56
|
+
log() { printf '\033[1;34m[build-libfirefly]\033[0m %s\n' "$*"; }
|
|
57
|
+
err() { printf '\033[1;31m[build-libfirefly]\033[0m %s\n' "$*" >&2; }
|
|
58
|
+
|
|
59
|
+
ensure_rustup_target() {
|
|
60
|
+
local target="$1"
|
|
61
|
+
if ! rustup target list --installed | grep -q "^${target}$"; then
|
|
62
|
+
log "installing rust target: $target"
|
|
63
|
+
rustup target add "$target"
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
build_ios() {
|
|
68
|
+
command -v rustup >/dev/null || { err "rustup not installed"; exit 1; }
|
|
69
|
+
command -v xcodebuild >/dev/null || { err "xcodebuild not installed"; exit 1; }
|
|
70
|
+
command -v lipo >/dev/null || { err "lipo not installed"; exit 1; }
|
|
71
|
+
|
|
72
|
+
log "iOS: ensuring rust targets installed"
|
|
73
|
+
for target in "${IOS_TARGETS[@]}"; do
|
|
74
|
+
ensure_rustup_target "$target"
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
for target in "${IOS_TARGETS[@]}"; do
|
|
78
|
+
log "iOS: building $target"
|
|
79
|
+
(cd "$CORE_DIR" && cargo build --release -p firefly --target "$target" $CARGO_FEATURES)
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
# Build the cargo dylibs (already done above). We then wrap each slice's
|
|
83
|
+
# dylib in a proper Firefly.framework bundle and ship those inside an
|
|
84
|
+
# XCFramework. This is the canonical Apple distribution shape and the only
|
|
85
|
+
# one CocoaPods accepts for dynamic linking via vendored_frameworks.
|
|
86
|
+
#
|
|
87
|
+
# Resulting layout:
|
|
88
|
+
# Firefly.xcframework/
|
|
89
|
+
# ios-arm64/Firefly.framework/{Firefly,Info.plist}
|
|
90
|
+
# ios-arm64_x86_64-simulator/Firefly.framework/{Firefly,Info.plist}
|
|
91
|
+
local stage device_fw sim_fw sim_universal_dylib
|
|
92
|
+
stage="$(mktemp -d)"
|
|
93
|
+
device_fw="$stage/device/${IOS_FRAMEWORK_NAME}.framework"
|
|
94
|
+
sim_fw="$stage/sim/${IOS_FRAMEWORK_NAME}.framework"
|
|
95
|
+
sim_universal_dylib="$stage/sim-universal-libfirefly.dylib"
|
|
96
|
+
|
|
97
|
+
log "iOS: lipo simulator slices (arm64-sim + x86_64)"
|
|
98
|
+
lipo -create \
|
|
99
|
+
"$CARGO_TARGET_DIR/aarch64-apple-ios-sim/release/libfirefly.dylib" \
|
|
100
|
+
"$CARGO_TARGET_DIR/x86_64-apple-ios/release/libfirefly.dylib" \
|
|
101
|
+
-output "$sim_universal_dylib"
|
|
102
|
+
|
|
103
|
+
ios_make_framework "$device_fw" \
|
|
104
|
+
"$CARGO_TARGET_DIR/aarch64-apple-ios/release/libfirefly.dylib" \
|
|
105
|
+
"iPhoneOS" "iphoneos"
|
|
106
|
+
ios_make_framework "$sim_fw" \
|
|
107
|
+
"$sim_universal_dylib" \
|
|
108
|
+
"iPhoneSimulator" "iphonesimulator"
|
|
109
|
+
|
|
110
|
+
log "iOS: assembling Firefly.xcframework"
|
|
111
|
+
rm -rf "$IOS_XCF_OUT"
|
|
112
|
+
mkdir -p "$(dirname "$IOS_XCF_OUT")"
|
|
113
|
+
xcodebuild -create-xcframework \
|
|
114
|
+
-framework "$device_fw" \
|
|
115
|
+
-framework "$sim_fw" \
|
|
116
|
+
-output "$IOS_XCF_OUT" >/dev/null
|
|
117
|
+
|
|
118
|
+
# Clean up artifacts from earlier strategies (fat dylib + two-dylib-resource).
|
|
119
|
+
rm -f "$PKG_DIR/ios/libfirefly.dylib" \
|
|
120
|
+
"$PKG_DIR/ios/libfirefly-device.dylib" \
|
|
121
|
+
"$PKG_DIR/ios/libfirefly-sim.dylib"
|
|
122
|
+
|
|
123
|
+
log "iOS: verifying sqlite3_firefly_init export in both slices"
|
|
124
|
+
for slice in "$IOS_XCF_OUT"/*/${IOS_FRAMEWORK_NAME}.framework/${IOS_FRAMEWORK_NAME}; do
|
|
125
|
+
if ! nm -gU "$slice" 2>/dev/null | grep -q "_sqlite3_firefly_init"; then
|
|
126
|
+
err "iOS: sqlite3_firefly_init not exported in $slice"
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
done
|
|
130
|
+
log "iOS: build complete -> $IOS_XCF_OUT"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Build a single Firefly.framework wrapping a Mach-O dylib. Apple's framework
|
|
134
|
+
# layout for iOS is flat (binary + Info.plist at the framework's root, no
|
|
135
|
+
# Versions/ symlink dance like macOS frameworks).
|
|
136
|
+
ios_make_framework() {
|
|
137
|
+
local fw_path="$1"
|
|
138
|
+
local source_dylib="$2"
|
|
139
|
+
local supported_platform="$3" # iPhoneOS or iPhoneSimulator
|
|
140
|
+
local platform_name="$4" # iphoneos or iphonesimulator
|
|
141
|
+
|
|
142
|
+
rm -rf "$fw_path"
|
|
143
|
+
mkdir -p "$fw_path"
|
|
144
|
+
|
|
145
|
+
# The binary inside a framework is named exactly like the framework, with
|
|
146
|
+
# no extension. The dylib's install_name must use @rpath so the embedded
|
|
147
|
+
# copy resolves at runtime.
|
|
148
|
+
cp "$source_dylib" "$fw_path/$IOS_FRAMEWORK_NAME"
|
|
149
|
+
install_name_tool -id "@rpath/${IOS_FRAMEWORK_NAME}.framework/${IOS_FRAMEWORK_NAME}" \
|
|
150
|
+
"$fw_path/$IOS_FRAMEWORK_NAME"
|
|
151
|
+
|
|
152
|
+
cat > "$fw_path/Info.plist" <<PLIST
|
|
153
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
154
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
155
|
+
<plist version="1.0">
|
|
156
|
+
<dict>
|
|
157
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
158
|
+
<string>en</string>
|
|
159
|
+
<key>CFBundleExecutable</key>
|
|
160
|
+
<string>${IOS_FRAMEWORK_NAME}</string>
|
|
161
|
+
<key>CFBundleIdentifier</key>
|
|
162
|
+
<string>${IOS_BUNDLE_ID}</string>
|
|
163
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
164
|
+
<string>6.0</string>
|
|
165
|
+
<key>CFBundleName</key>
|
|
166
|
+
<string>${IOS_FRAMEWORK_NAME}</string>
|
|
167
|
+
<key>CFBundlePackageType</key>
|
|
168
|
+
<string>FMWK</string>
|
|
169
|
+
<key>CFBundleShortVersionString</key>
|
|
170
|
+
<string>1.0</string>
|
|
171
|
+
<key>CFBundleVersion</key>
|
|
172
|
+
<string>1</string>
|
|
173
|
+
<key>CFBundleSupportedPlatforms</key>
|
|
174
|
+
<array>
|
|
175
|
+
<string>${supported_platform}</string>
|
|
176
|
+
</array>
|
|
177
|
+
<key>DTPlatformName</key>
|
|
178
|
+
<string>${platform_name}</string>
|
|
179
|
+
<key>MinimumOSVersion</key>
|
|
180
|
+
<string>${IOS_DEPLOYMENT_TARGET}</string>
|
|
181
|
+
</dict>
|
|
182
|
+
</plist>
|
|
183
|
+
PLIST
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
build_android() {
|
|
187
|
+
command -v rustup >/dev/null || { err "rustup not installed"; exit 1; }
|
|
188
|
+
if ! command -v cargo-ndk >/dev/null; then
|
|
189
|
+
log "android: installing cargo-ndk"
|
|
190
|
+
cargo install cargo-ndk
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
log "android: ensuring rust targets installed"
|
|
194
|
+
for entry in "${ANDROID_TARGETS[@]}"; do
|
|
195
|
+
local rust_target="${entry%% *}"
|
|
196
|
+
ensure_rustup_target "$rust_target"
|
|
197
|
+
done
|
|
198
|
+
|
|
199
|
+
for entry in "${ANDROID_TARGETS[@]}"; do
|
|
200
|
+
local rust_target="${entry%% *}"
|
|
201
|
+
local abi="${entry##* }"
|
|
202
|
+
local out_dir="$ANDROID_JNI_OUT/$abi"
|
|
203
|
+
log "android: building $rust_target -> $abi"
|
|
204
|
+
mkdir -p "$out_dir"
|
|
205
|
+
(cd "$CORE_DIR" && cargo ndk --target "$rust_target" --platform 24 -- \
|
|
206
|
+
build --release -p firefly $CARGO_FEATURES)
|
|
207
|
+
cp "$CARGO_TARGET_DIR/$rust_target/release/libfirefly.so" "$out_dir/libfirefly.so"
|
|
208
|
+
done
|
|
209
|
+
|
|
210
|
+
log "android: build complete; .so files at $ANDROID_JNI_OUT"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main() {
|
|
214
|
+
local mode="${1:-all}"
|
|
215
|
+
case "$mode" in
|
|
216
|
+
ios) build_ios ;;
|
|
217
|
+
android) build_android ;;
|
|
218
|
+
all) build_ios; build_android ;;
|
|
219
|
+
*) err "unknown mode: $mode (use ios|android|all)"; exit 1 ;;
|
|
220
|
+
esac
|
|
221
|
+
log "done"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
main "$@"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
+
import type { EventSubscription } from 'expo-modules-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Payload of the `FireflyChange` event emitted by the native module each
|
|
6
|
+
* time libfirefly's notify channel signals a local CRDT-relevant write
|
|
7
|
+
* for a database that has at least one registered listener.
|
|
8
|
+
*
|
|
9
|
+
* `blob` is the raw `encode_rowstates` output — the same shape
|
|
10
|
+
* `firefly_changes()` returns and `firefly_apply()` consumes — so a
|
|
11
|
+
* subscription handler can ship it upstream over a WebSocket without
|
|
12
|
+
* touching it.
|
|
13
|
+
*/
|
|
14
|
+
export interface FireflyChangePayload {
|
|
15
|
+
/** Listener handle — matches the value returned by `addChangeListener`. */
|
|
16
|
+
id: number;
|
|
17
|
+
/** RowState batch encoded by `crate::codec::encode_rowstates`. */
|
|
18
|
+
blob: Uint8Array;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type FireflyChangeListener = (payload: FireflyChangePayload) => void;
|
|
22
|
+
|
|
23
|
+
// Self-contained surface for the native module — we only declare the
|
|
24
|
+
// methods we actually call. Expo Modules' `NativeModule` type alias
|
|
25
|
+
// resolves to a constructor type rather than an instance type, so we
|
|
26
|
+
// skip the inheritance and just spell out the EventEmitter methods we
|
|
27
|
+
// use (addListener / removeAllListeners).
|
|
28
|
+
export type FireflyClientNativeModule = {
|
|
29
|
+
/** Entry-point symbol passed to sqlite3_load_extension. */
|
|
30
|
+
entryPoint: string;
|
|
31
|
+
/**
|
|
32
|
+
* Absolute path to the bundled libfirefly extension, with the platform
|
|
33
|
+
* suffix stripped (SQLite's sqlite3_load_extension re-appends it). Throws
|
|
34
|
+
* if the dylib/so isn't found in the host app's bundle.
|
|
35
|
+
*/
|
|
36
|
+
getLibraryPath(): string;
|
|
37
|
+
getEntryPoint(): string;
|
|
38
|
+
/**
|
|
39
|
+
* Register a native change listener against the SQLite file at `path`.
|
|
40
|
+
* Every local CRDT-relevant write to that database fires the
|
|
41
|
+
* `FireflyChange` event with the encoded RowState batch. Returns the
|
|
42
|
+
* listener id; pass it to `removeChangeListener` to stop receiving
|
|
43
|
+
* events. Throws on invalid path / in-memory DB.
|
|
44
|
+
*/
|
|
45
|
+
addChangeListener(path: string): number;
|
|
46
|
+
/** Unregister a listener previously returned by `addChangeListener`. */
|
|
47
|
+
removeChangeListener(id: number): void;
|
|
48
|
+
// EventEmitter surface inherited from NativeModule.
|
|
49
|
+
addListener(eventName: 'FireflyChange', listener: FireflyChangeListener): EventSubscription;
|
|
50
|
+
removeAllListeners(eventName: 'FireflyChange'): void;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const FireflyClientModule =
|
|
54
|
+
requireNativeModule<FireflyClientNativeModule>('FireflyClient');
|
|
55
|
+
|
|
56
|
+
export default FireflyClientModule;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Subscribe to local CRDT writes against the database file at `path`.
|
|
60
|
+
* `handler` receives the encoded RowState batch — pass it to your
|
|
61
|
+
* upstream sender (e.g. `ws.send(blob)`). Returns a subscription whose
|
|
62
|
+
* `.remove()` tears down both the JS event listener and the native
|
|
63
|
+
* libfirefly listener.
|
|
64
|
+
*
|
|
65
|
+
* Multiple subscribers per path are supported; each gets its own handle
|
|
66
|
+
* and only sees events tagged with its own id.
|
|
67
|
+
*/
|
|
68
|
+
export function subscribeToChanges(
|
|
69
|
+
path: string,
|
|
70
|
+
handler: (blob: Uint8Array) => void,
|
|
71
|
+
): EventSubscription {
|
|
72
|
+
const id = FireflyClientModule.addChangeListener(path);
|
|
73
|
+
const sub = FireflyClientModule.addListener('FireflyChange', (event) => {
|
|
74
|
+
if (event.id === id) handler(event.blob);
|
|
75
|
+
});
|
|
76
|
+
// Wrap so callers can `.remove()` both halves with one call.
|
|
77
|
+
return {
|
|
78
|
+
remove() {
|
|
79
|
+
sub.remove();
|
|
80
|
+
FireflyClientModule.removeChangeListener(id);
|
|
81
|
+
},
|
|
82
|
+
} as EventSubscription;
|
|
83
|
+
}
|