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
18 changes: 15 additions & 3 deletions js/net/src/ietf/publisher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,8 @@ interface ServedGroup {
sequence: number;
/** Whether the header claimed the stream starts at the group's first object. */
firstObject: boolean;
/** Whether the header claimed the stream's FIN ends the group (END_OF_GROUP). */
endOfGroup: boolean;
/** Each object's absolute id (reconstructed from its delta) and payload. */
objects: { id: number; payload: string }[];
}
Expand Down Expand Up @@ -1623,7 +1625,12 @@ async function readGroup(stream: ReadableStream<Uint8Array>): Promise<ServedGrou
objects.push({ id, payload: new TextDecoder().decode(payload) });
}

return { sequence: header.groupId, firstObject: header.flags.firstObject, objects };
return {
sequence: header.groupId,
firstObject: header.flags.firstObject,
endOfGroup: header.flags.hasEnd,
objects,
};
}

/** Read a stream carrying only an END_OF_TRACK object, returning the group it names. */
Expand Down Expand Up @@ -1684,7 +1691,8 @@ async function readFill(stream: ReadableStream<Uint8Array>): Promise<ServedFill>
/**
* An absolute filter names the objects it wants, so the boundary groups are trimmed to it
* and the groups outside it are never opened. The first object written carries its absolute
* id, or the subscriber would read a silently renumbered group.
* id, or the subscriber would read a silently renumbered group, and a capped tail does not
* claim END_OF_GROUP, or the subscriber would think the group ended at the cap.
*/
test("draft-20: an absolute filter trims the range it serves", async () => {
const fx = fixture();
Expand Down Expand Up @@ -1715,6 +1723,7 @@ test("draft-20: an absolute filter trims the range it serves", async () => {
sequence: 1,
// The head was trimmed, so the stream does not start at the group's first object.
firstObject: false,
endOfGroup: true,
objects: [
{ id: 1, payload: "1.1" },
{ id: 2, payload: "1.2" },
Expand All @@ -1726,6 +1735,8 @@ test("draft-20: an absolute filter trims the range it serves", async () => {
expect(await readGroup(second)).toEqual({
sequence: 2,
firstObject: true,
// The filter ends at object 0 of 3, so the stream stops before the group does.
endOfGroup: false,
objects: [{ id: 0, payload: "2.0" }],
});

Expand Down Expand Up @@ -1787,6 +1798,7 @@ test("draft-20: a fill serves the current group's head on a fetch stream", async
expect(await readGroup(live)).toEqual({
sequence: 0,
firstObject: false,
endOfGroup: true,
objects: [{ id: 2, payload: "0.2" }],
});
} finally {
Expand Down Expand Up @@ -1863,7 +1875,7 @@ test("draft-20: a backwards range within one group serves nothing and ends the s
try {
const served = await nextUni(fx.uni);
if (!served) throw new Error("the group stream never opened");
expect(await readGroup(served)).toEqual({ sequence: 0, firstObject: false, objects: [] });
expect(await readGroup(served)).toEqual({ sequence: 0, firstObject: false, endOfGroup: false, objects: [] });
} finally {
fx.close();
client.close();
Expand Down
4 changes: 3 additions & 1 deletion js/net/src/ietf/publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,9 @@ export class Publisher {
hasExtensions: stamped,
hasSubgroup: false,
hasSubgroupObject: false,
hasEnd: true,
// A stream capped by the range stops before the group may end, so its FIN
// cannot claim END_OF_GROUP.
hasEnd: slice.until === undefined,
hasPriority: true,
// Only honest when the stream really starts at the group's first object;
// a trimmed head starts partway through.
Expand Down
39 changes: 39 additions & 0 deletions rs/moq-net/src/ietf/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2410,6 +2410,9 @@ impl<S: crate::transport::poll::Session> TrackServe<S> {
flags: ietf::GroupFlags {
has_extensions: self.timescale.is_some(),
first_object: slice.skip == 0,
// A stream capped by the range stops before the group may end, so its
// FIN cannot claim END_OF_GROUP.
has_end: slice.until.is_none(),
..Default::default()
},
};
Expand Down Expand Up @@ -4490,6 +4493,42 @@ mod serve_tests {
);
}

/// A stream cut short by the range's end Location does not claim END_OF_GROUP, since
/// the group goes on past it. A range ending on a whole group still does. A cap at or past
/// the group's real end clears the bit too, since the header is written before we know.
#[tokio::test]
async fn capped_group_does_not_claim_its_end() {
async fn serve(object: Option<u64>) -> ietf::GroupHeader {
let log = crate::lite::test_transport::Log::default();
let session = SinkSession::new(log.clone());

let track = track::Producer::new(std::sync::Arc::new(crate::broadcast::Info::default()), "test", None);
let subscriber = track.subscribe(None);

let mut group = track.append_group().unwrap();
for payload in [b"aa", b"bb", b"cc"] {
group.write_frame(crate::Timestamp::ZERO, payload.as_slice()).unwrap();
}
group.finish().unwrap();
track.finish().unwrap();

let range = ServeRange {
start: Some(Location { group: 0, object: 0 }),
end: Some(EndLocation { group: 0, object }),
};
let mut serve = TrackServe::new(session, subscriber, RequestId(0), Version::Draft20, range, None);
kio::wait(|waiter| serve.poll(waiter)).await.unwrap();

let mut buf = bytes::Bytes::from(log.writes.lock().unwrap().clone());
ietf::GroupHeader::decode(&mut buf, Version::Draft20).expect("a group header")
}

assert!(!serve(Some(0)).await.flags.has_end, "capped at object 0 of 3");
assert!(!serve(Some(2)).await.flags.has_end, "capped at the last object");
assert!(!serve(Some(3)).await.flags.has_end, "capped past the last object");
assert!(serve(None).await.flags.has_end, "the whole group");
}

const ALL_VERSIONS: [Version; 9] = [
Version::Draft14,
Version::Draft15,
Expand Down
Loading