Skip to content

Commit 2ef2ea3

Browse files
committed
WIP: expo module
1 parent e3108e4 commit 2ef2ea3

File tree

12 files changed

+401
-0
lines changed

12 files changed

+401
-0
lines changed

expo/iroh-expo/android/build.gradle

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
apply plugin: 'com.android.library'
2+
3+
group = 'expo.modules.irohexpo'
4+
version = '0.6.0'
5+
6+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7+
apply from: expoModulesCorePlugin
8+
applyKotlinExpoModulesCorePlugin()
9+
useCoreDependencies()
10+
useExpoPublishing()
11+
12+
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
13+
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
14+
// Most of the time, you may like to manage the Android SDK versions yourself.
15+
def useManagedAndroidSdkVersions = false
16+
if (useManagedAndroidSdkVersions) {
17+
useDefaultAndroidSdkVersions()
18+
} else {
19+
buildscript {
20+
// Simple helper that allows the root project to override versions declared by this library.
21+
ext.safeExtGet = { prop, fallback ->
22+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
23+
}
24+
}
25+
project.android {
26+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
27+
defaultConfig {
28+
minSdkVersion safeExtGet("minSdkVersion", 21)
29+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
30+
}
31+
}
32+
}
33+
34+
android {
35+
namespace "expo.modules.irohexpo"
36+
defaultConfig {
37+
versionCode 1
38+
versionName "0.6.0"
39+
}
40+
lintOptions {
41+
abortOnError false
42+
}
43+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<manifest>
2+
</manifest>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package expo.modules.irohexpo
2+
3+
import expo.modules.kotlin.modules.Module
4+
import expo.modules.kotlin.modules.ModuleDefinition
5+
6+
class IrohExpoModule : Module() {
7+
// Each module class must implement the definition function. The definition consists of components
8+
// that describes the module's functionality and behavior.
9+
// See https://docs.expo.dev/modules/module-api for more details about available components.
10+
override fun definition() = ModuleDefinition {
11+
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
12+
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
13+
// The module will be accessible from `requireNativeModule('IrohExpo')` in JavaScript.
14+
Name("IrohExpo")
15+
16+
// Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary.
17+
Constants(
18+
"PI" to Math.PI
19+
)
20+
21+
// Defines event names that the module can send to JavaScript.
22+
Events("onChange")
23+
24+
// Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
25+
Function("hello") {
26+
"Hello world! 👋"
27+
}
28+
29+
// Defines a JavaScript function that always returns a Promise and whose native code
30+
// is by default dispatched on the different thread than the JavaScript runtime runs on.
31+
AsyncFunction("setValueAsync") { value: String ->
32+
// Send an event to JavaScript.
33+
sendEvent("onChange", mapOf(
34+
"value" to value
35+
))
36+
}
37+
38+
// Enables the module to be used as a native view. Definition components that are accepted as part of
39+
// the view definition: Prop, Events.
40+
View(IrohExpoView::class) {
41+
// Defines a setter for the `name` prop.
42+
Prop("name") { view: IrohExpoView, prop: String ->
43+
println(prop)
44+
}
45+
}
46+
}
47+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"platforms": ["ios", "tvos", "android", "web"],
3+
"ios": {
4+
"modules": ["IrohExpoModule"]
5+
},
6+
"android": {
7+
"modules": ["expo.modules.irohexpo.IrohExpoModule"]
8+
}
9+
}

expo/iroh-expo/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Import the native module. On web, it will be resolved to IrohExpo.web.ts
2+
// and on native platforms to IrohExpo.ts
3+
import IrohExpoModule from './src/IrohExpoModule';
4+
import { AddrInfoOptions, ChangeEventPayload, ShareMode } from './src/IrohExpo.types';
5+
6+
export async function nodeId(): Promise<string> {
7+
return IrohExpoModule.nodeId();
8+
}
9+
10+
export async function docCreate(): Promise<Doc> {
11+
let id = await IrohExpoModule.docCreate();
12+
return new Doc(id);
13+
}
14+
15+
export async function docDrop(id: string) {
16+
return await IrohExpoModule.docDrop(id);
17+
}
18+
19+
export async function docJoin(ticket: string): Promise<Doc> {
20+
let id = await IrohExpoModule.docJoin(ticket);
21+
return new Doc(id);
22+
}
23+
24+
25+
export class Doc {
26+
public id: string;
27+
28+
constructor(id: string) {
29+
this.id = id;
30+
}
31+
32+
async share(mode: ShareMode, addrOptions: AddrInfoOptions) {
33+
return IrohExpoModule.docShare(this.id, mode, addrOptions);
34+
}
35+
}

expo/iroh-expo/ios/Documents.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import ExpoModulesCore
2+
import IrohLib
3+
4+
/**
5+
* Options when creating a ticket
6+
*/
7+
public enum AddrInfoOptionsString: String, Enumerable {
8+
/**
9+
* Only the Node ID is added.
10+
*
11+
* This usually means that iroh-dns discovery is used to find address information.
12+
*/
13+
case id
14+
/**
15+
* Include both the relay URL and the direct addresses.
16+
*/
17+
case relayAndAddresses
18+
/**
19+
* Only include the relay URL.
20+
*/
21+
case relay
22+
/**
23+
* Only include the direct addresses.
24+
*/
25+
case addresses
26+
}
27+
28+
// convert to AddrInfoOptions
29+
extension AddrInfoOptionsString {
30+
func toAddrInfoOptions() -> AddrInfoOptions {
31+
switch self {
32+
case .id:
33+
return AddrInfoOptions.id
34+
case .addresses:
35+
return AddrInfoOptions.addresses
36+
case .relay:
37+
return AddrInfoOptions.relay
38+
case .relayAndAddresses:
39+
return AddrInfoOptions.relayAndAddresses
40+
}
41+
}
42+
}
43+
44+
/**
45+
* Intended capability for document share tickets
46+
*/
47+
public enum ShareModeString: String, Enumerable {
48+
/**
49+
* Read-only access
50+
*/
51+
case read
52+
/**
53+
* Write access
54+
*/
55+
case write
56+
}
57+
58+
// convert to ShareMode
59+
extension ShareModeString {
60+
func toShareMode() -> ShareMode {
61+
switch self {
62+
case .read:
63+
return ShareMode.read
64+
case .write:
65+
return ShareMode.write
66+
}
67+
}
68+
}

expo/iroh-expo/ios/IrohExpo.podspec

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Pod::Spec.new do |s|
2+
s.name = 'IrohExpo'
3+
s.version = '1.0.0'
4+
s.summary = 'A sample project summary'
5+
s.description = 'A sample project description'
6+
s.author = ''
7+
s.homepage = 'https://docs.expo.dev/modules/'
8+
s.platforms = { :ios => '15.4', :tvos => '15.4' }
9+
s.source = { git: '' }
10+
s.static_framework = true
11+
12+
s.dependency 'ExpoModulesCore'
13+
s.dependency 'IrohLib'
14+
15+
# Swift/Objective-C compatibility
16+
s.pod_target_xcconfig = {
17+
'DEFINES_MODULE' => 'YES',
18+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
19+
}
20+
21+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
22+
end
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import ExpoModulesCore
2+
import IrohLib
3+
4+
public class IrohExpoModule: Module {
5+
private var node: IrohNode?
6+
7+
private func irohPath() -> URL {
8+
let paths = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)
9+
let irohPath = paths[0].appendingPathComponent("iroh")
10+
mkdirP(path: irohPath.path)
11+
return irohPath
12+
}
13+
14+
public func definition() -> ModuleDefinition {
15+
Name("IrohExpo")
16+
17+
OnCreate {
18+
do {
19+
// IrohLib.setLogLevel(level: .debug)
20+
// try IrohLib.startMetricsCollection()
21+
let path = self.irohPath()
22+
self.node = try IrohNode(path: path.path)
23+
} catch {
24+
print("error creating iroh node \(error)")
25+
}
26+
}
27+
28+
AsyncFunction("nodeId") {
29+
return self.node?.nodeId()
30+
}
31+
32+
AsyncFunction("docCreate") {
33+
guard let doc = try self.node?.docCreate() else {
34+
throw IrohError.Doc(description: "error creating doc")
35+
}
36+
return doc.id();
37+
}
38+
39+
AsyncFunction("docDrop") { (docId: String) in
40+
return try self.node?.docDrop(id: docId)
41+
}
42+
43+
AsyncFunction("docOpen") { (docId: String) in
44+
return try self.node?.docOpen(id: docId)
45+
}
46+
47+
AsyncFunction("docJoin") { (ticket: String) in
48+
return try self.node?.docJoin(ticket: ticket)
49+
}
50+
51+
AsyncFunction("docShare") { (docId: String, mode: ShareModeString, addrOptions: AddrInfoOptionsString) in
52+
guard let doc = try self.node?.docOpen(id: docId) else {
53+
throw IrohError.Doc(description: "error opening doc")
54+
}
55+
return try doc.share(mode: mode.toShareMode(), addrOptions: addrOptions.toAddrInfoOptions())
56+
}
57+
58+
AsyncFunction("docSetBytes") { (docId: string, author: AuthorId, key: Data, bytes: Data) in
59+
guard let doc = try self.node?.docOpen(id: docId) else {
60+
throw IrohError.Doc(description: "error opening doc")
61+
}
62+
63+
return try doc.setBytes(author: author, key: key, bytes: bytes)
64+
}
65+
}
66+
}
67+
68+
func mkdirP(path: String) {
69+
let fileManager = FileManager.default
70+
do {
71+
try fileManager.createDirectory(atPath: path,
72+
withIntermediateDirectories: true,
73+
attributes: nil)
74+
} catch {
75+
print("Error creating directory: \(error)")
76+
}
77+
}
78+
79+
80+
81+
/// A representation of a mutable, synchronizable key-value store.
82+
interface Doc {
83+
/// Get the document id of this doc.
84+
string id();
85+
/// Close the document.
86+
void close();
87+
/// Set the content of a key to a byte array.
88+
Hash set_bytes([ByRef] AuthorId author, bytes key, bytes value);
89+
/// Set an entries on the doc via its key, hash, and size.
90+
void set_hash(AuthorId author, bytes key, Hash hash, u64 size);
91+
/// Add an entry from an absolute file path
92+
void import_file(AuthorId author, bytes key, string path, boolean in_place, DocImportFileCallback? cb);
93+
/// Export an entry as a file to a given absolute path
94+
void export_file(Entry entry, string path, DocExportFileCallback? cb);
95+
/// Delete entries that match the given `author` and key `prefix`.
96+
///
97+
/// This inserts an empty entry with the key set to `prefix`, effectively clearing all other
98+
/// entries whose key starts with or is equal to the given `prefix`.
99+
///
100+
/// Returns the number of entries deleted.
101+
u64 del(AuthorId author_id, bytes prefix);
102+
/// Get the latest entry for a key and author.
103+
Entry? get_one(Query query);
104+
/// Get entries.
105+
///
106+
/// Note: this allocates for each `Entry`, if you have many `Entry`s this may be a prohibitively large list.
107+
/// Please file an [issue](https://github.com/n0-computer/iroh-ffi/issues/new) if you run into this issue
108+
sequence<Entry> get_many(Query query);
109+
/// Get an entry for a key and author.
110+
///
111+
/// Optionally also get the entry if it is empty (i.e. a deletion marker)
112+
Entry? get_exact(AuthorId author, bytes key, boolean include_empty);
113+
114+
/// Share this document with peers over a ticket.
115+
string share(ShareMode mode, AddrInfoOptions addr_options);
116+
/// Start to sync this document with a list of peers.
117+
void start_sync(sequence<NodeAddr> peers);
118+
/// Stop the live sync for this document.
119+
void leave();
120+
/// Subscribe to events for this document.
121+
void subscribe(SubscribeCallback cb);
122+
/// Get status info for this document
123+
OpenState status();
124+
/// Set the download policy for this document
125+
void set_download_policy(DownloadPolicy policy);
126+
/// Get the download policy for this document
127+
DownloadPolicy get_download_policy();
128+
};

expo/iroh-expo/src/IrohExpo.types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export type ChangeEventPayload = {
2+
value: string;
3+
};
4+
5+
/// Options passed to [`IrohNode.new`]. Controls the behaviour of an iroh node.
6+
export type NodeOptions = {
7+
/// How frequently the blob store should clean up unreferenced blobs, in milliseconds.
8+
/// Set to 0 to disable gc
9+
GCIntervalMillis: number
10+
};
11+
12+
13+
/// Intended capability for document share tickets
14+
export enum ShareMode {
15+
/// Read-only access
16+
read = "read",
17+
/// Write access
18+
write = "write",
19+
};
20+
21+
22+
/// Options when creating a ticket
23+
export enum AddrInfoOptions {
24+
/// Only the Node ID is added.
25+
///
26+
/// This usually means that iroh-dns discovery is used to find address information.
27+
id = "id",
28+
/// Include both the relay URL and the direct addresses.
29+
relayAndAddresses = "relayAndAddresses",
30+
/// Only include the relay URL.
31+
relay = "relay",
32+
/// Only include the direct addresses.
33+
addresses = "addresses",
34+
};

0 commit comments

Comments
 (0)