No description
Find a file
2026-06-08 12:33:37 +03:00
.dart_tool init 2026-06-07 15:05:57 +03:00
lib file download actor 2026-06-08 12:33:37 +03:00
test file download actor 2026-06-08 12:33:37 +03:00
.gitignore file actor 2026-06-07 15:26:11 +03:00
analysis_options.yaml file actor 2026-06-07 15:26:11 +03:00
devbox.json init 2026-06-07 15:05:57 +03:00
devbox.lock init 2026-06-07 15:05:57 +03:00
GUIDELINES.md code guidelines 2026-06-07 15:22:41 +03:00
pubspec.lock basic nostr actors 2026-06-07 16:45:43 +03:00
pubspec.yaml file upload actor 2026-06-08 11:08:00 +03:00
README.md file download actor 2026-06-08 12:33:37 +03:00

swarm

Actor-based business logic library. Each actor runs in an isolated Dart isolate; you send it a message and await a result. Actors are created with a factory tear-off so the handler is constructed inside the isolate:

final actor = Actor.create(MyActor.new);
final result = await actor.send(MyMessage());
await actor.stop();

All examples below call handle() directly (no isolate), which is how the unit tests work. In production code use Actor.create + send.


FileActor

Local file I/O — text and binary.

final actor = FileActor();

No constructor parameters.

Message Result Description
LoadFile(path) FileLoaded(content) Read file as UTF-8 string
SaveFile(path, content) FileSaved() Write string to file
LoadBinaryFile(path) BinaryFileLoaded(bytes) Read file as Uint8List
SaveBinaryFile(path, bytes) FileSaved() (same type) Write Uint8List to file
final actor = FileActor();

// Text
await actor.handle(SaveFile('/tmp/note.txt', 'hello'));
final r = await actor.handle(LoadFile('/tmp/note.txt'));
print((r as FileLoaded).content); // hello

// Binary
await actor.handle(SaveBinaryFile('/tmp/data.bin', bytes));
final rb = await actor.handle(LoadBinaryFile('/tmp/data.bin'));
final loaded = (rb as BinaryFileLoaded).bytes;

NostrSignerActor

Signs Nostr events (NIP-01) using a secp256k1 private key.

final actor = NostrSignerActor(privateKeyHex);
Parameter Type Description
privateKey String 64-char hex private key
Message Result Description
SignEvent(kind, content, tags?, createdAt?) EventSigned(event) Returns a signed event as a JSON string with all seven NIP-01 fields
GetPublicKey() PublicKeyResult(publicKey) Returns the hex public key for this signer

tags defaults to []. createdAt defaults to DateTime.now() (Unix seconds).

final signer = NostrSignerActor(privkeyHex);

final pk = await signer.handle(const GetPublicKey());
print((pk as PublicKeyResult).publicKey);

final signed = await signer.handle(
  const SignEvent(kind: 1, content: 'hello world'),
);
final eventJson = (signed as EventSigned).event; // ready to publish

NostrPublishActor

Publishes signed Nostr events to one or more relays and collects OK/rejection outcomes. Retries failed relay connections with exponential back-off.

final actor = NostrPublishActor(
  relayUrls: ['wss://relay.example.com'],
  retryConfig: const RetryConfig(),   // optional
  batchTimeout: const Duration(seconds: 10),  // optional
);
Parameter Type Default Description
relayUrls List<String> required Relay WebSocket URLs
retryConfig RetryConfig see below Retry behaviour
batchTimeout Duration 10 s How long to wait for OK per relay

RetryConfig

Field Default Description
maxAttempts 3 Max connection attempts per relay
baseDelay 1 s Initial retry delay
maxDelay 32 s Cap on retry delay
jitter true Add ±25 % randomisation to delay
Message Result Description
PublishBatch(events) BatchPublished(outcomes) Publishes a list of JSON event strings; outcomes maps relay URL → RelayConfirmed, RelayRejected(reason), or RelayFailed

Events must be fully signed JSON strings (e.g. from NostrSignerActor).

final signer = NostrSignerActor(privkeyHex);
final publisher = NostrPublishActor(
  relayUrls: ['wss://relay.otherwhere.app/'],
);

final signed = await signer.handle(
  const SignEvent(kind: 1, content: 'hello'),
);
final result = await publisher.handle(
  PublishBatch([(signed as EventSigned).event]),
);
final outcomes = (result as BatchPublished).outcomes;
for (final entry in outcomes.entries) {
  print('${entry.key}: ${entry.value}');
  // wss://relay.otherwhere.app/: RelayConfirmed
}

NostrFetchActor

Fetches Nostr events from one or more relays using a NIP-01 filter. Collects events until EOSE or a deadline, deduplicates by event ID.

final actor = NostrFetchActor(
  relayUrls: ['wss://relay.example.com'],
  eoseDeadline: const Duration(seconds: 5),  // optional
);
Parameter Type Default Description
relayUrls List<String> required Relay WebSocket URLs
eoseDeadline Duration 5 s Max wait per relay for EOSE
Message Result Description
FetchEvents(filter) EventsFetched(events) filter is a JSON string (NIP-01 filter object); events is a deduplicated list of JSON event strings
final fetcher = NostrFetchActor(
  relayUrls: ['wss://relay.otherwhere.app/'],
);

final result = await fetcher.handle(
  FetchEvents('{"kinds":[1],"authors":["$pubkeyHex"],"limit":10}'),
);
final events = (result as EventsFetched).events;
for (final e in events) {
  print(jsonDecode(e)['content']);
}

GeoActor

Geohash encoding, decoding, and neighbour lookup via the dart_geohash library.

final actor = GeoActor();

No constructor parameters.

Message Result Description
EncodeLocation(latitude, longitude, precision?) GeohashEncoded(geohash) Encode lat/lon to geohash string; precision defaults to 9
DecodeGeohash(geohash) LocationDecoded(latitude, longitude) Decode geohash to coordinates
NeighborsOf(geohash) NeighborsResult(neighbors) Returns a Map<String, String> with keys NORTH, NORTHEAST, EAST, SOUTHEAST, SOUTH, SOUTHWEST, WEST, NORTHWEST, CENTRAL
final geo = GeoActor();

final enc = await geo.handle(
  const EncodeLocation(latitude: 48.8566, longitude: 2.3522, precision: 7),
);
print((enc as GeohashEncoded).geohash); // u09tunq

final dec = await geo.handle(DecodeGeohash('u09tunq'));
print((dec as LocationDecoded).latitude);  // ~48.856
print((dec as LocationDecoded).longitude); // ~2.352

final nb = await geo.handle(NeighborsOf('u09tunq'));
print((nb as NeighborsResult).neighbors['NORTH']); // u09tunu

FileDownloadActor

Downloads a file from a URL and saves it to disk via an injected FileActor.

final actor = FileDownloadActor(
  fileActor: FileActor(),
);
Parameter Type Default Description
fileActor FileActor required Actor used to write the downloaded bytes to disk
httpClient http.Client? new client Injectable HTTP client; provide a MockClient in tests

Call actor.close() when done to release the HTTP client.

Message Result Description
DownloadFile(url, savePath) FileDownloaded(savePath) GETs the URL and writes the response bytes to savePath. Throws if the response status is not 2xx.
final actor = FileDownloadActor(fileActor: FileActor());

final result = await actor.handle(
  const DownloadFile(
    url: 'https://example.com/image.jpg',
    savePath: '/tmp/image.jpg',
  ),
);
print((result as FileDownloaded).savePath); // /tmp/image.jpg

await actor.close();

FileUploadActor

Uploads files to a Nostr-authenticated remote storage service. Handles NIP-44 encrypted registration (kind 5392) and NIP-98 signed upload requests (kind 27235). Returns a permanent public CDN URL.

final actor = FileUploadActor(
  privateKey: userPrivkeyHex,
  appName: kAppName,
  appSecret: kAppSecret,
);
Parameter Type Default Description
privateKey String required User's 64-char hex Nostr private key
appName String required App identifier registered with the service
appSecret String required Per-app shared secret (keep out of public source)
apiBaseUrl String https://upload.otherwhere.app API base URL
relayUrl String wss://relay.otherwhere.app/ Nostr relay for registration events

Call actor.close() when done to release HTTP connections.

Message Result Description
RegisterForUpload() UploaderRegistered() Publishes a kind 5392 event to the relay with the NIP-44 encrypted app secret. Call once per user; re-registering is safe (idempotent server-side).
UploadFile(bytes, contentType) FileUploaded(cdnUrl) Requests a presigned PUT URL, uploads the file directly to storage, and returns the permanent CDN URL. Throws if bytes.length > 10 MB. Accepted content types: image/jpeg, image/png, image/webp, image/gif.

Registration should be called once and the result persisted (e.g. in flutter_secure_storage). If the flag is lost, calling it again is harmless. Do not block the upload flow on registration — unregistered users get a small daily upload allowance while the registration event propagates.

const storage = FlutterSecureStorage();
const _kRegisteredKey = 'upload_registered_$kAppName';

final actor = FileUploadActor(
  privateKey: await storage.read(key: 'nostr_privkey'),
  appName: kAppName,
  appSecret: kAppSecret,
);

// Register once
if (await storage.read(key: _kRegisteredKey) != 'true') {
  await actor.handle(const RegisterForUpload());
  await storage.write(key: _kRegisteredKey, value: 'true');
}

// Upload
final result = await actor.handle(
  UploadFile(bytes: imageBytes, contentType: 'image/jpeg'),
);
final url = (result as FileUploaded).cdnUrl;
// https://cdn.otherwhere.app/<app>/<pubkey>/<uuid>.jpg

NostrAccountActor

Manages a Nostr user identity: keypair lifecycle, BIP-39 mnemonic support (NIP-06 derivation), and kind-0 profile fetch/publish via injected helper actors.

final actor = NostrAccountActor(
  fetcher: NostrFetchActor(relayUrls: ['wss://relay.example.com']),
  publisher: NostrPublishActor(relayUrls: ['wss://relay.example.com']),
);
Parameter Type Default Description
fetcher NostrFetchActor? null Required for FetchProfile
publisher NostrPublishActor? null Required for UpdateProfile

Both helpers are optional at construction; passing neither is valid when only using key management messages.

Message Result Description
CreateAccount() AccountCreated(publicKeyHex, mnemonic) Generates a new keypair from 128-bit entropy via BIP-39 + NIP-06 derivation
ImportAccount(privateKeyHex) AccountImported(publicKeyHex) Loads an existing raw hex private key. ExportMnemonic returns null afterwards.
ImportMnemonic(mnemonic) AccountImported(publicKeyHex) Derives keypair from a 12-word BIP-39 mnemonic via NIP-06 path m/44'/1237'/0'/0/0
GetAccountPublicKey() AccountPublicKeyResult(publicKeyHex) Returns the loaded account's public key
ExportPrivateKey() PrivateKeyExported(privateKeyHex) Returns the raw private key
ExportMnemonic() MnemonicExported(mnemonic?) Returns the BIP-39 mnemonic, or null if the account was imported via raw private key
FetchProfile() ProfileFetched(profile?) Fetches kind-0 from relay; caches result for GetPublicData
UpdateProfile(profile) ProfileUpdated() Publishes a kind-0 event; caches profile for GetPublicData
GetPublicData() PublicData(data) Returns a Map<String, dynamic> with pubkey plus any cached profile fields

NostrProfile fields (all optional): name, displayName, about, picture, banner, website, nip05, lud16.

All messages except Create/Import throw StateError if called before the account is loaded. FetchProfile throws if no fetcher was provided; UpdateProfile throws if no publisher was provided.

final fetcher = NostrFetchActor(relayUrls: ['wss://relay.otherwhere.app/']);
final publisher = NostrPublishActor(relayUrls: ['wss://relay.otherwhere.app/']);
final actor = NostrAccountActor(fetcher: fetcher, publisher: publisher);

// New user
final created = await actor.handle(const CreateAccount()) as AccountCreated;
print(created.mnemonic);      // 12-word backup phrase
print(created.publicKeyHex);  // hex pubkey

// Returning user — restore from mnemonic
await actor.handle(ImportMnemonic(savedMnemonic));

// Or restore from raw key
await actor.handle(ImportAccount(savedPrivkeyHex));

// Update profile
await actor.handle(
  UpdateProfile(const NostrProfile(name: 'alice', nip05: 'alice@example.com')),
);

// Fetch latest profile from relay
final fetched = await actor.handle(const FetchProfile()) as ProfileFetched;
print(fetched.profile?.name);

// Public-facing data (pubkey + cached profile)
final pub = await actor.handle(const GetPublicData()) as PublicData;
print(pub.data); // {'pubkey': '...', 'name': 'alice', 'nip05': 'alice@example.com'}