readme
This commit is contained in:
parent
f55083ea1b
commit
305cff62bd
1 changed files with 267 additions and 0 deletions
267
README.md
Normal file
267
README.md
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
# 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:
|
||||
|
||||
```dart
|
||||
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.
|
||||
|
||||
```dart
|
||||
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 |
|
||||
|
||||
```dart
|
||||
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.
|
||||
|
||||
```dart
|
||||
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).
|
||||
|
||||
```dart
|
||||
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.
|
||||
|
||||
```dart
|
||||
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`).
|
||||
|
||||
```dart
|
||||
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.
|
||||
|
||||
```dart
|
||||
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 |
|
||||
|
||||
```dart
|
||||
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](https://pub.dev/packages/dart_geohash) library.
|
||||
|
||||
```dart
|
||||
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` |
|
||||
|
||||
```dart
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
```dart
|
||||
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.
|
||||
|
||||
```dart
|
||||
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
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue