Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions dart/moq/lib/src/aliases.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,32 @@ typedef Instance = MoqInstance;
/// A route over a prefix: its origin-relative path, required wildcard captures (empty without wildcards), and route metadata.
typedef Announce = MoqAnnounce;

/// What an [AnnounceConsumer] yields: [AnnounceEventPresent] or
/// [AnnounceEventAbsent].
/// What an [AnnounceConsumer] yields: [AnnounceEventPresent],
/// [AnnounceEventUnavailable], [AnnounceEventAvailable], [AnnounceEventAbsent],
/// or [AnnounceEventOffline].
///
/// Another publisher instance taking a path over keeps it present: hold a
/// [Following] to see the broadcast change.
typedef AnnounceEvent = MoqAnnounceEvent;

/// A route now covers the path, which was absent.
/// A route now covers the path, which was absent; it may arrive unavailable.
typedef AnnounceEventPresent = PresentMoqAnnounceEvent;

/// No route covers the path any more; carries its last route.
/// The present path lost every usable path to its origin but may recover one:
/// it stays present while requests for it may fail.
typedef AnnounceEventUnavailable = UnavailableMoqAnnounceEvent;

/// The present path recovered a usable path: request it again.
typedef AnnounceEventAvailable = AvailableMoqAnnounceEvent;

/// The origin withdrew the path; carries its last route.
typedef AnnounceEventAbsent = AbsentMoqAnnounceEvent;

/// This side let go of the path, which its origin may still serve: the session
/// it arrived on closed, or the network lost every path to it. Carries its last
/// route.
typedef AnnounceEventOffline = OfflineMoqAnnounceEvent;

/// The write side of a broadcast: publish tracks into it.
typedef BroadcastProducer = MoqBroadcastProducer;

Expand Down
2 changes: 1 addition & 1 deletion dart/moq/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ final class Moq {
MoqRoute route(String path, {RouteOptions? options}) =>
session.publish().route(path: path, options: options ?? RouteOptions());

/// List the paths matching [options] as each becomes present or absent;
/// List the paths matching [options] as each becomes present, absent, or offline, or changes availability;
/// prefixes stay relative to the origin.
///
/// Listen to `announced(...).updates()` for a [Stream] of [AnnounceEvent]
Expand Down
154 changes: 151 additions & 3 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3011,7 +3011,12 @@ class FfiConverterMoqOriginConfig {
class MoqRouteInfo {
final int cost;
final MoqInstance instance;
MoqRouteInfo({required this.cost, required this.instance});
final bool available;
MoqRouteInfo({
required this.cost,
required this.instance,
required this.available,
});
}

class FfiConverterMoqRouteInfo {
Expand All @@ -3031,8 +3036,13 @@ class FfiConverterMoqRouteInfo {
);
final instance = instance_lifted.value;
new_offset += instance_lifted.bytesRead;
final available_lifted = FfiConverterBool.read(
Uint8List.view(buf.buffer, new_offset),
);
final available = available_lifted.value;
new_offset += available_lifted.bytesRead;
return LiftRetVal(
MoqRouteInfo(cost: cost, instance: instance),
MoqRouteInfo(cost: cost, instance: instance, available: available),
new_offset - buf.offsetInBytes,
);
}
Expand All @@ -3041,6 +3051,7 @@ class FfiConverterMoqRouteInfo {
final total_length =
FfiConverterUInt64.allocationSize(value.cost) +
FfiConverterMoqInstance.allocationSize(value.instance) +
FfiConverterBool.allocationSize(value.available) +
0;
final buf = Uint8List(total_length);
write(value, buf);
Expand All @@ -3057,12 +3068,17 @@ class FfiConverterMoqRouteInfo {
value.instance,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterBool.write(
value.available,
Uint8List.view(buf.buffer, new_offset),
);
return new_offset - buf.offsetInBytes;
}

static int allocationSize(MoqRouteInfo value) {
return FfiConverterUInt64.allocationSize(value.cost) +
FfiConverterMoqInstance.allocationSize(value.instance) +
FfiConverterBool.allocationSize(value.available) +
0;
}
}
Expand Down Expand Up @@ -8410,11 +8426,29 @@ class FfiConverterMoqAnnounceEvent {
lifted.bytesRead - subview.offsetInBytes + 4,
);
case 2:
final lifted = UnavailableMoqAnnounceEvent.read(subview);
return LiftRetVal<MoqAnnounceEvent>(
lifted.value,
lifted.bytesRead - subview.offsetInBytes + 4,
);
case 3:
final lifted = AvailableMoqAnnounceEvent.read(subview);
return LiftRetVal<MoqAnnounceEvent>(
lifted.value,
lifted.bytesRead - subview.offsetInBytes + 4,
);
case 4:
final lifted = AbsentMoqAnnounceEvent.read(subview);
return LiftRetVal<MoqAnnounceEvent>(
lifted.value,
lifted.bytesRead - subview.offsetInBytes + 4,
);
case 5:
final lifted = OfflineMoqAnnounceEvent.read(subview);
return LiftRetVal<MoqAnnounceEvent>(
lifted.value,
lifted.bytesRead - subview.offsetInBytes + 4,
);
default:
throw UniffiInternalError(
UniffiInternalError.unexpectedEnumCase,
Expand Down Expand Up @@ -8474,6 +8508,82 @@ class PresentMoqAnnounceEvent extends MoqAnnounceEvent {
}
}

class UnavailableMoqAnnounceEvent extends MoqAnnounceEvent {
final MoqAnnounce announce;
UnavailableMoqAnnounceEvent(MoqAnnounce this.announce);
UnavailableMoqAnnounceEvent._(MoqAnnounce this.announce);
static LiftRetVal<UnavailableMoqAnnounceEvent> read(Uint8List buf) {
int new_offset = buf.offsetInBytes;
final announce_lifted = FfiConverterMoqAnnounce.read(
Uint8List.view(buf.buffer, new_offset),
);
final announce = announce_lifted.value;
new_offset += announce_lifted.bytesRead;
return LiftRetVal(UnavailableMoqAnnounceEvent._(announce), new_offset);
}

@override
RustBuffer lower() {
final buf = Uint8List(allocationSize());
write(buf);
return toRustBuffer(buf);
}

@override
int allocationSize() {
return FfiConverterMoqAnnounce.allocationSize(announce) + 4;
}

@override
int write(Uint8List buf) {
buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 2);
int new_offset = buf.offsetInBytes + 4;
new_offset += FfiConverterMoqAnnounce.write(
announce,
Uint8List.view(buf.buffer, new_offset),
);
return new_offset;
}
}

class AvailableMoqAnnounceEvent extends MoqAnnounceEvent {
final MoqAnnounce announce;
AvailableMoqAnnounceEvent(MoqAnnounce this.announce);
AvailableMoqAnnounceEvent._(MoqAnnounce this.announce);
static LiftRetVal<AvailableMoqAnnounceEvent> read(Uint8List buf) {
int new_offset = buf.offsetInBytes;
final announce_lifted = FfiConverterMoqAnnounce.read(
Uint8List.view(buf.buffer, new_offset),
);
final announce = announce_lifted.value;
new_offset += announce_lifted.bytesRead;
return LiftRetVal(AvailableMoqAnnounceEvent._(announce), new_offset);
}

@override
RustBuffer lower() {
final buf = Uint8List(allocationSize());
write(buf);
return toRustBuffer(buf);
}

@override
int allocationSize() {
return FfiConverterMoqAnnounce.allocationSize(announce) + 4;
}

@override
int write(Uint8List buf) {
buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 3);
int new_offset = buf.offsetInBytes + 4;
new_offset += FfiConverterMoqAnnounce.write(
announce,
Uint8List.view(buf.buffer, new_offset),
);
return new_offset;
}
}

class AbsentMoqAnnounceEvent extends MoqAnnounceEvent {
final MoqAnnounce announce;
AbsentMoqAnnounceEvent(MoqAnnounce this.announce);
Expand Down Expand Up @@ -8502,7 +8612,45 @@ class AbsentMoqAnnounceEvent extends MoqAnnounceEvent {

@override
int write(Uint8List buf) {
buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 2);
buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 4);
int new_offset = buf.offsetInBytes + 4;
new_offset += FfiConverterMoqAnnounce.write(
announce,
Uint8List.view(buf.buffer, new_offset),
);
return new_offset;
}
}

class OfflineMoqAnnounceEvent extends MoqAnnounceEvent {
final MoqAnnounce announce;
OfflineMoqAnnounceEvent(MoqAnnounce this.announce);
OfflineMoqAnnounceEvent._(MoqAnnounce this.announce);
static LiftRetVal<OfflineMoqAnnounceEvent> read(Uint8List buf) {
int new_offset = buf.offsetInBytes;
final announce_lifted = FfiConverterMoqAnnounce.read(
Uint8List.view(buf.buffer, new_offset),
);
final announce = announce_lifted.value;
new_offset += announce_lifted.bytesRead;
return LiftRetVal(OfflineMoqAnnounceEvent._(announce), new_offset);
}

@override
RustBuffer lower() {
final buf = Uint8List(allocationSize());
write(buf);
return toRustBuffer(buf);
}

@override
int allocationSize() {
return FfiConverterMoqAnnounce.allocationSize(announce) + 4;
}

@override
int write(Uint8List buf) {
buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 5);
int new_offset = buf.offsetInBytes + 4;
new_offset += FfiConverterMoqAnnounce.write(
announce,
Expand Down
9 changes: 6 additions & 3 deletions doc/concept/identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,12 @@ goes offline; JavaScript `origin.follow(path)`, whose `active` is undefined
between instances; the moq-ffi bindings `follow(path)`, whose `available()`
returns null once the path goes offline; and C `moq_origin_follow`, whose
callback reports `MOQ_FOLLOW_EVENT_OFFLINE`.
An announcement listing (`announced()`) only reports a path becoming present or
absent: an instance change keeps it present, so a reader that needs one holds a
follower. `@moq/watch`, `moq play`, and `moqsrc` follow this way, and
An announcement listing (`announced()`) reports a path becoming present,
unavailable while the network holds its route without a path to it, available
again once it recovers one, and finally absent once the origin withdraws it or
offline once this side lets go of it, which says nothing of whether the origin
still serves it. An instance change keeps it present, so a reader that needs
one holds a follower. `@moq/watch`, `moq play`, and `moqsrc` follow this way, and
`moq export ts --stitch` can splice a replacement as a marked program switch. A
request failing is never the signal to restart; the follower handing out a new
broadcast is. Nor is a track's error kind: a track withdrawn
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/go/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ for event, err := range announced.All(ctx) {
// handed each new run, with Available returning nil while the path is offline.
present, ok := event.(moq.AnnounceEventPresent)
if !ok {
continue // AnnounceEventAbsent
continue // absent, offline, or a change in availability
}
ann := present.Announce
// Prefix stays origin-relative; Captures is a list, empty without wildcards.
Expand Down
3 changes: 2 additions & 1 deletion doc/lib/kt/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ Moq.connect("https://relay.example.com", ClientConfig(tls = ClientTls(roots = li
// each new run, or null once it goes offline.
val announce = when (event) {
is AnnounceEventPresent -> event.announce
is AnnounceEventAbsent -> return@collect
// Absent, offline, or a change in availability.
else -> return@collect
}
// Prefixes stay origin-relative; captures is a list of wildcard matches, empty without wildcards.
println(announce.captures)
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/py/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async def main():
# A path stays present across publisher restarts: hold client.follow(path) to
# be handed each new run, or None once it goes offline.
if not isinstance(event, moq.AnnounceEventPresent):
continue # AnnounceEventAbsent
continue # absent, offline, or a change in availability
announcement = event.announce
print(announcement.captures) # list of wildcard matches, empty without wildcards
broadcast = await client.request(announcement.prefix)
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/rs/moq-net.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ What you use it for, beyond what the concept page already describes:
- **Routes report what the mesh filled in.** An announcement's `route` gives its `instance()` and `cost()`; an application sets only `RouteOptions`. The hop chain a relay uses for loop detection stays internal. An origin's own hop id is `origin::Config::hop`, random unless a relay names one.
- **Requests can pin an instance.** `consumer.request(path, Some(instance))` resolves only through a route announcing that [publisher instance](/concept/moq-lite#publisher-instances); `None` takes whichever route wins. The resolved consumer names its instance in `info().instance`, pinned or not. `origin.route(path, options)` serves a random instance minted when it is created and kept across `withdraw()` and `announce()`. Unless one is named, a claim moves to a fresh instance each time a job holding its current one ends, and a job takes the claim's. Set one with `RouteOptions::with_instance(instance, epoch)` only for a publisher that continues its group sequences across restarts and raises the epoch each time. `route.instance()` names it.
- **Rank several CDNs.** Dial each with its own subscribe handle, `origin.clone().with_preference(origin::Preference::new(rank))` (higher wins; 0 is neutral and a negative rank demotes), all on one origin. Requests use the preferred link that has a route and fail over to the next; see [multiple CDNs](/concept/moq-lite#multiple-cdns).
- **Request one instance, or follow them all.** `consumer.request(path, None)` resolves the instance serving a path now, and fails `Unroutable` only once discovery has synced with no route for it. `consumer.follow(path, None)` hands out each publisher instance serving a path from `available()`: the first once announced, then each one that takes over, `None` once the path goes offline, and whatever serves it next, like JS `follow(path)`. `consumer.announced()` lists paths only as present or absent. A broadcast already resolved stays on the old instance until dropped or its route goes. See [identity and recovery](/concept/identity) to choose between following and holding one instance.
- **Request one instance, or follow them all.** `consumer.request(path, None)` resolves the instance serving a path now, and fails `Unroutable` only once discovery has synced with no route for it. `consumer.follow(path, None)` hands out each publisher instance serving a path from `available()`: the first once announced, then each one that takes over, `None` once the path goes offline, and whatever serves it next, like JS `follow(path)`. `consumer.announced()` lists paths as present, then unavailable or available as a held route loses and recovers its path, and finally absent once the origin withdraws one or offline once this side lets go of it, which says nothing of whether the origin still serves it. A broadcast already resolved stays on the old instance until dropped or its route goes. See [identity and recovery](/concept/identity) to choose between following and holding one instance.
- **Discovery follows what you watch.** A client session asks the relay for announcements only under the prefixes its subscribe origin's readers watch, with `announced()`, `request`, or `follow`, stops asking about a prefix 2s after nothing at or beneath it is watched, and asks again on reconnect. A broadcast resolved through a discovered route keeps its path watched until dropped. `request` owns discovery of its path, so no separate watch is needed. A server session, or a client fed into an origin handle marked `with_link(origin::Link::Peer)` or `Link::Upstream`, asks for everything it may, so a relay discovers everything. An HTTP gateway (the moq-hls `Server`, the moq-webrtc WHEP router) watches its whole scope while it lives, so the paths its callers name never open a request each. moq-lite-03/04 sessions, whose announcements mark no initial set, still ask for everything.
- **Relay by opting into proxying.** On moq-lite-07 (`moq-lite-07-wip`, opt-in) a session sends its peer only the routes to its origin's own claims, so an application never becomes transit for anyone else. A relay built on the library marks the handle it publishes with `origin.clone().with_proxy(true)` to carry what it learned from other peers too; `moq-relay` does so for every session. Older versions advertise everything either way.
- **Dial and accept sockets.** `socket::Endpoint::new(&origin, prefix, id)` is a server, a client, or both: its first dial announces a hidden client endpoint at `<prefix>/.client/<id>`, and its first accept a hidden server endpoint at `<prefix>/.server/<id>`, both used only to route sessions, so grants decide which roles it may take. A session speaks one of two protocols, each with its own dial and accept. `accept_tcp()` and `accept_track()` yield an `Incoming` dial to `accept` or `reject` (dropping it rejects). `dial_tcp(server)` and an accepted `tcp` dial yield a `socket::TcpStream`, a `group::Producer` and `group::Consumer` pair carrying one reliable, ordered byte pipe each way (finish is FIN, abort is RST, and `ready` paces the writer to the reader); `dial_track(server, info)` and an accepted `track` dial yield a `socket::TrackStream`, this side's `track::Producer` and a `track::Subscriber` of the peer's half, subscribed reliably and in order until the application updates it, one message per group. A dial resolves once the peer accepts, and an accept checks that the claimed dialer really dialed this endpoint, waiting for its endpoint while the dial stays open. Every hop needs moq-lite-07 or moq-transport: while the origin has an older moq-lite client session whose publish half doesn't opt into hidden broadcasts (`with_hidden(true)`), dials and accepts fail with `Unsupported`. A dial to a peer behind such a hop waits, as for a peer not online yet. Session tracks are never announced or listed. See [Sockets](/concept/socket).
Expand Down
Loading
Loading