updated spec to 2.1, changed almost everything

other small fixes
This commit is contained in:
Green Sky 2024-11-28 12:39:57 +01:00
parent 34dc01d4dc
commit 741f1428d3
No known key found for this signature in database
9 changed files with 82 additions and 319 deletions

View File

@ -46,8 +46,8 @@ target_link_libraries(solanaceae_ngcft1 PUBLIC
add_library(solanaceae_ngchs2 add_library(solanaceae_ngchs2
./solanaceae/ngc_hs2/ngc_hs2_send.hpp ./solanaceae/ngc_hs2/ngc_hs2_send.hpp
./solanaceae/ngc_hs2/ngc_hs2_send.cpp ./solanaceae/ngc_hs2/ngc_hs2_send.cpp
./solanaceae/ngc_hs2/ngc_hs2_recv.hpp #./solanaceae/ngc_hs2/ngc_hs2_recv.hpp
./solanaceae/ngc_hs2/ngc_hs2_recv.cpp #./solanaceae/ngc_hs2/ngc_hs2_recv.cpp
) )
target_include_directories(solanaceae_ngchs2 PUBLIC .) target_include_directories(solanaceae_ngchs2 PUBLIC .)
target_compile_features(solanaceae_ngchs2 PUBLIC cxx_std_17) target_compile_features(solanaceae_ngchs2 PUBLIC cxx_std_17)

View File

@ -56,7 +56,9 @@ void NGCFT1::updateSendTransfer(float time_delta, uint32_t group_number, uint32_
timeouts_set.erase({idx, id}); timeouts_set.erase({idx, id});
can_packet_size -= data.size(); can_packet_size -= data.size();
} else { } else {
#if 0 // too spammy
std::cerr << "NGCFT1 warning: no space to resend timedout\n"; std::cerr << "NGCFT1 warning: no space to resend timedout\n";
#endif
} }
} }
}); });

View File

@ -75,20 +75,7 @@ enum class NGCFT1_file_kind : uint32_t {
// https://gist.github.com/Green-Sky/440cd9817a7114786850eb4c62dc57c3 // https://gist.github.com/Green-Sky/440cd9817a7114786850eb4c62dc57c3
// id: ts start, ts end // id: ts start, ts end
// content: HS2_RANGE_TIME = 0x00000f00, // TODO: remove, did not survive
// - ts start (do we need this? when this is part of the id?) HS2_RANGE_TIME_MSGPACK = 0x00000f02,
// - ts end (same)
// - list size
// - ppk
// - mid
// - ts
HS2_INFO_RANGE_TIME = 0x00000f00,
// TODO: half open ranges
// TODO: id based
// TODO: ppk based?
// id: ppk, mid, ts
HS2_SINGLE_MESSAGE,
// TODO: message pack
}; };

View File

@ -257,7 +257,7 @@ std::vector<ChunkPicker::ContentChunkR> ChunkPicker::updateChunkRequests(
assert(objreg.valid(participating_in_last)); assert(objreg.valid(participating_in_last));
auto it = participating_unfinished.find(participating_in_last); auto it = participating_unfinished.find(participating_in_last);
// hard limit robin rounds to array size time 20 // hard limit robin rounds to array size times 20
for (size_t i = 0; req_ret.size() < num_requests && i < participating_unfinished.size()*20; i++, it++) { for (size_t i = 0; req_ret.size() < num_requests && i < participating_unfinished.size()*20; i++, it++) {
if (it == participating_unfinished.end()) { if (it == participating_unfinished.end()) {
it = participating_unfinished.begin(); it = participating_unfinished.begin();
@ -375,7 +375,6 @@ std::vector<ChunkPicker::ContentChunkR> ChunkPicker::updateChunkRequests(
} }
} }
//if (it == participating_unfinished.end() || ++it == participating_unfinished.end()) {
if (it == participating_unfinished.end()) { if (it == participating_unfinished.end()) {
participating_in_last = entt::null; participating_in_last = entt::null;
} else { } else {

View File

@ -1,113 +0,0 @@
#include "./ngc_hs2_recv.hpp"
#include <solanaceae/tox_contacts/tox_contact_model2.hpp>
NGCHS2Recv::NGCHS2Recv(
Contact3Registry& cr,
RegistryMessageModelI& rmm,
ToxContactModel2& tcm,
ToxEventProviderI& tep,
NGCFT1& nft
) :
_cr(cr),
_rmm(rmm),
_rmm_sr(_rmm.newSubRef(this)),
_tcm(tcm),
_tep_sr(tep.newSubRef(this)),
_nft(nft),
_nftep_sr(_nft.newSubRef(this))
{
_rmm_sr
.subscribe(RegistryMessageModel_Event::message_construct)
.subscribe(RegistryMessageModel_Event::message_updated)
.subscribe(RegistryMessageModel_Event::message_destroy)
;
_tep_sr
.subscribe(TOX_EVENT_GROUP_PEER_JOIN)
.subscribe(TOX_EVENT_GROUP_PEER_EXIT)
;
_nftep_sr
.subscribe(NGCFT1_Event::recv_request)
.subscribe(NGCFT1_Event::recv_init)
.subscribe(NGCFT1_Event::recv_data)
.subscribe(NGCFT1_Event::send_data)
.subscribe(NGCFT1_Event::recv_done)
.subscribe(NGCFT1_Event::send_done)
;
}
NGCHS2Recv::~NGCHS2Recv(void) {
}
float NGCHS2Recv::iterate(float delta) {
return 1000.f;
}
bool NGCHS2Recv::onEvent(const Message::Events::MessageConstruct&) {
return false;
}
bool NGCHS2Recv::onEvent(const Message::Events::MessageUpdated&) {
return false;
}
bool NGCHS2Recv::onEvent(const Message::Events::MessageDestory&) {
return false;
}
bool NGCHS2Recv::onEvent(const Events::NGCFT1_recv_request& e) {
if (
e.file_kind != NGCFT1_file_kind::HS2_INFO_RANGE_TIME &&
e.file_kind != NGCFT1_file_kind::HS2_SINGLE_MESSAGE
) {
return false; // not for us
}
return false;
}
bool NGCHS2Recv::onEvent(const Events::NGCFT1_recv_init& e) {
if (
e.file_kind != NGCFT1_file_kind::HS2_INFO_RANGE_TIME &&
e.file_kind != NGCFT1_file_kind::HS2_SINGLE_MESSAGE
) {
return false; // not for us
}
return false;
}
bool NGCHS2Recv::onEvent(const Events::NGCFT1_recv_data&) {
return false;
}
bool NGCHS2Recv::onEvent(const Events::NGCFT1_send_data&) {
return false;
}
bool NGCHS2Recv::onEvent(const Events::NGCFT1_recv_done&) {
return false;
}
bool NGCHS2Recv::onEvent(const Events::NGCFT1_send_done&) {
return false;
}
bool NGCHS2Recv::onToxEvent(const Tox_Event_Group_Peer_Join* e) {
const auto group_number = tox_event_group_peer_join_get_group_number(e);
const auto peer_number = tox_event_group_peer_join_get_peer_id(e);
const auto c = _tcm.getContactGroupPeer(group_number, peer_number);
assert(c);
// add to check list with inital cooldown
return false;
}
bool NGCHS2Recv::onToxEvent(const Tox_Event_Group_Peer_Exit* e) {
return false;
}

View File

@ -1,83 +0,0 @@
#pragma once
#include <solanaceae/toxcore/tox_event_interface.hpp>
#include <solanaceae/contact/contact_model3.hpp>
#include <solanaceae/message3/registry_message_model.hpp>
#include <solanaceae/ngc_ft1/ngcft1.hpp>
#include <entt/container/dense_map.hpp>
// fwd
class ToxContactModel2;
// time ranges
// should we just do last x minutes like zngchs?
// properly done, we need to use:
// - Message::Components::ViewCurserBegin
// - Message::Components::ViewCurserEnd
//
// on startup, manually check all registries for ranges (meh) (do later)
// listen on message events, check if range, see if range satisfied recently
// deal with a queue, and delay (at least 1sec, 3-10sec after a peer con change)
// or we always overrequest (eg 48h), and only fetch messages in, or close to range
class NGCHS2Recv : public RegistryMessageModelEventI, public ToxEventI, public NGCFT1EventI {
Contact3Registry& _cr;
RegistryMessageModelI& _rmm;
RegistryMessageModelI::SubscriptionReference _rmm_sr;
ToxContactModel2& _tcm;
ToxEventProviderI::SubscriptionReference _tep_sr;
NGCFT1& _nft;
NGCFT1EventProviderI::SubscriptionReference _nftep_sr;
// describes our knowlage of a remote peer
struct RemoteInfo {
// list of all ppk+mid+ts they sent us (filtered by reqs, like range, ppk...)
// with when it last sent a range? hmm
};
entt::dense_map<Contact3, RemoteInfo> _remote_info;
// open/running info requests (by c)
// open/running info responses (by c)
static const bool _only_send_self_observed {true};
static const int64_t _max_time_into_past_default {60}; // s
public:
NGCHS2Recv(
Contact3Registry& cr,
RegistryMessageModelI& rmm,
ToxContactModel2& tcm,
ToxEventProviderI& tep,
NGCFT1& nf
);
~NGCHS2Recv(void);
float iterate(float delta);
// add to queue with timer
// check and updates all existing cursers for giving reg in queue
void enqueueWantCurser(Message3Handle m);
protected:
bool onEvent(const Message::Events::MessageConstruct&) override;
bool onEvent(const Message::Events::MessageUpdated&) override;
bool onEvent(const Message::Events::MessageDestory&) override;
protected:
bool onEvent(const Events::NGCFT1_recv_request&) override;
bool onEvent(const Events::NGCFT1_recv_init&) override;
bool onEvent(const Events::NGCFT1_recv_data&) override;
bool onEvent(const Events::NGCFT1_send_data&) override;
bool onEvent(const Events::NGCFT1_recv_done&) override;
bool onEvent(const Events::NGCFT1_send_done&) override;
protected:
bool onToxEvent(const Tox_Event_Group_Peer_Join* e) override;
bool onToxEvent(const Tox_Event_Group_Peer_Exit* e) override;
};

View File

@ -12,7 +12,7 @@
namespace Components { namespace Components {
void IncommingInfoRequestQueue::queueRequest(const InfoRequest& new_request) { void IncommingTimeRangeRequestQueue::queueRequest(const TimeRangeRequest& new_request) {
// TODO: do more than exact dedupe // TODO: do more than exact dedupe
for (const auto& [ts_start, ts_end] : _queue) { for (const auto& [ts_start, ts_end] : _queue) {
if (ts_start == new_request.ts_start && ts_end == new_request.ts_end) { if (ts_start == new_request.ts_start && ts_end == new_request.ts_end) {
@ -23,15 +23,6 @@ void IncommingInfoRequestQueue::queueRequest(const InfoRequest& new_request) {
_queue.push_back(new_request); _queue.push_back(new_request);
} }
void IncommingMsgRequestQueue::queueRequest(const SingleMessageRequest& new_request) {
for (const auto& [ppk, mid, ts] : _queue) {
if (mid == new_request.mid && ts == new_request.ts && ppk == new_request.ppk) {
return; // already enqueued
}
}
_queue.push_back(new_request);
}
} // Components } // Components
@ -75,7 +66,7 @@ float NGCHS2Send::iterate(float delta) {
auto fn_iirq = [this](auto&& view) { auto fn_iirq = [this](auto&& view) {
for (auto&& [cv, iirq] : view.each()) { for (auto&& [cv, iirq] : view.each()) {
Contact3Handle c{_cr, cv}; Contact3Handle c{_cr, cv};
auto& iirr = c.get_or_emplace<Components::IncommingInfoRequestRunning>(); auto& iirr = c.get_or_emplace<Components::IncommingTimeRangeRequestRunning>();
// dedup queued from running // dedup queued from running
@ -87,38 +78,15 @@ float NGCHS2Send::iterate(float delta) {
} }
}; };
auto fn_imrq = [this](auto&& view) {
for (auto&& [cv, imrq] : view.each()) {
Contact3Handle c{_cr, cv};
auto& imrr = c.get_or_emplace<Components::IncommingMsgRequestRunning>();
// dedup queued from running
if (imrr._list.size() >= _max_parallel_per_peer) {
continue;
}
// new ft here?
}
};
// first handle range requests on weak self // first handle range requests on weak self
//for (auto&& [cv, iirq] : _cr.view<Contact::Components::TagSelfWeak, Components::IncommingInfoRequestQueue>().each()) { //for (auto&& [cv, iirq] : _cr.view<Contact::Components::TagSelfWeak, Components::IncommingTimeRangeRequestQueue>().each()) {
fn_iirq(_cr.view<Contact::Components::TagSelfWeak, Components::IncommingInfoRequestQueue>()); fn_iirq(_cr.view<Contact::Components::TagSelfWeak, Components::IncommingTimeRangeRequestQueue>());
// then handle messages on weak self
//for (auto&& [cv, imrq] : _cr.view<Contact::Components::TagSelfWeak, Components::IncommingMsgRequestQueue>().each()) {
fn_imrq(_cr.view<Contact::Components::TagSelfWeak, Components::IncommingMsgRequestQueue>());
// we could stop here, if too much is already running // we could stop here, if too much is already running
// then range on others // then range on others
//for (auto&& [cv, iirq] : _cr.view<Components::IncommingInfoRequestQueue>(entt::exclude_t<Contact::Components::TagSelfWeak>{}).each()) { //for (auto&& [cv, iirq] : _cr.view<Components::IncommingTimeRangeRequestQueue>(entt::exclude_t<Contact::Components::TagSelfWeak>{}).each()) {
fn_iirq(_cr.view<Components::IncommingInfoRequestQueue>(entt::exclude_t<Contact::Components::TagSelfWeak>{})); fn_iirq(_cr.view<Components::IncommingTimeRangeRequestQueue>(entt::exclude_t<Contact::Components::TagSelfWeak>{}));
// then messages on others
//for (auto&& [cv, imrq] : _cr.view<Components::IncommingMsgRequestQueue>(entt::exclude_t<Contact::Components::TagSelfWeak>{}).each()) {
fn_imrq(_cr.view<Components::IncommingMsgRequestQueue>(entt::exclude_t<Contact::Components::TagSelfWeak>{}));
return 1000.f; return 1000.f;
} }
@ -146,7 +114,7 @@ static uint64_t deserlTS(ByteSpan ts_bytes) {
return deserlSimpleType<uint64_t>(ts_bytes); return deserlSimpleType<uint64_t>(ts_bytes);
} }
void NGCHS2Send::handleRange(Contact3Handle c, const Events::NGCFT1_recv_request& e) { void NGCHS2Send::handleTimeRange(Contact3Handle c, const Events::NGCFT1_recv_request& e) {
ByteSpan fid{e.file_id, e.file_id_size}; ByteSpan fid{e.file_id, e.file_id_size};
// TODO: better size check // TODO: better size check
if (fid.size != sizeof(uint64_t)+sizeof(uint64_t)) { if (fid.size != sizeof(uint64_t)+sizeof(uint64_t)) {
@ -172,12 +140,13 @@ void NGCHS2Send::handleRange(Contact3Handle c, const Events::NGCFT1_recv_request
// dedupe insert into queue // dedupe insert into queue
// how much overlap do we allow? // how much overlap do we allow?
c.get_or_emplace<Components::IncommingInfoRequestQueue>().queueRequest({ c.get_or_emplace<Components::IncommingTimeRangeRequestQueue>().queueRequest({
ts_start, ts_start,
ts_end, ts_end,
}); });
} }
#if 0
void NGCHS2Send::handleSingleMessage(Contact3Handle c, const Events::NGCFT1_recv_request& e) { void NGCHS2Send::handleSingleMessage(Contact3Handle c, const Events::NGCFT1_recv_request& e) {
ByteSpan fid{e.file_id, e.file_id_size}; ByteSpan fid{e.file_id, e.file_id_size};
// TODO: better size check // TODO: better size check
@ -208,13 +177,6 @@ void NGCHS2Send::handleSingleMessage(Contact3Handle c, const Events::NGCFT1_recv
return; return;
} }
// file content
// - message type (text/textaction/file(ft1sha1))
// - if text/textaction
// - text (string)
// - else if file
// - file type
// - file id
// for queue, we need group, peer, msg_ppk, msg_mid, msg_ts // for queue, we need group, peer, msg_ppk, msg_mid, msg_ts
@ -225,6 +187,7 @@ void NGCHS2Send::handleSingleMessage(Contact3Handle c, const Events::NGCFT1_recv
ts, ts,
}); });
} }
#endif
bool NGCHS2Send::onEvent(const Message::Events::MessageConstruct&) { bool NGCHS2Send::onEvent(const Message::Events::MessageConstruct&) {
return false; return false;
@ -240,8 +203,7 @@ bool NGCHS2Send::onEvent(const Message::Events::MessageDestory&) {
bool NGCHS2Send::onEvent(const Events::NGCFT1_recv_request& e) { bool NGCHS2Send::onEvent(const Events::NGCFT1_recv_request& e) {
if ( if (
e.file_kind != NGCFT1_file_kind::HS2_INFO_RANGE_TIME && e.file_kind != NGCFT1_file_kind::HS2_RANGE_TIME_MSGPACK
e.file_kind != NGCFT1_file_kind::HS2_SINGLE_MESSAGE
) { ) {
return false; // not for us return false; // not for us
} }
@ -267,10 +229,8 @@ bool NGCHS2Send::onEvent(const Events::NGCFT1_recv_request& e) {
// - out of max time range (ft specific, not a quick_allow) // - out of max time range (ft specific, not a quick_allow)
} }
if (e.file_kind == NGCFT1_file_kind::HS2_INFO_RANGE_TIME) { if (e.file_kind == NGCFT1_file_kind::HS2_RANGE_TIME_MSGPACK) {
handleRange(c, e); handleTimeRange(c, e);
} else if (e.file_kind == NGCFT1_file_kind::HS2_SINGLE_MESSAGE) {
handleSingleMessage(c, e);
} }
return true; return true;

View File

@ -17,51 +17,28 @@
class ToxContactModel2; class ToxContactModel2;
struct InfoRequest { struct TimeRangeRequest {
uint64_t ts_start{0}; uint64_t ts_start{0};
uint64_t ts_end{0}; uint64_t ts_end{0};
}; };
struct SingleMessageRequest {
ByteSpan ppk;
uint32_t mid {0};
uint64_t ts {0}; // deciseconds
};
// TODO: move to own file // TODO: move to own file
namespace Components { namespace Components {
struct IncommingInfoRequestQueue { struct IncommingTimeRangeRequestQueue {
std::vector<InfoRequest> _queue; std::vector<TimeRangeRequest> _queue;
// we should remove/notadd queued requests // we should remove/notadd queued requests
// that are subsets of same or larger ranges // that are subsets of same or larger ranges
void queueRequest(const InfoRequest& new_request); void queueRequest(const TimeRangeRequest& new_request);
}; };
struct IncommingInfoRequestRunning { struct IncommingTimeRangeRequestRunning {
struct Entry { struct Entry {
InfoRequest ir; TimeRangeRequest ir;
std::vector<uint8_t> data; // trasfer data in memory std::vector<uint8_t> data; // trasfer data in memory
}; };
entt::dense_map<uint8_t, Entry> _list; entt::dense_map<uint8_t, Entry> _list;
}; };
struct IncommingMsgRequestQueue {
// optimize dup lookups (this list could be large)
std::vector<SingleMessageRequest> _queue;
// removes dups
void queueRequest(const SingleMessageRequest& new_request);
};
struct IncommingMsgRequestRunning {
struct Entry {
SingleMessageRequest smr;
std::vector<uint8_t> data; // trasfer data in memory
};
// make more efficent? this list is very short
entt::dense_map<uint8_t, Entry> _list;
};
} // Components } // Components
class NGCHS2Send : public RegistryMessageModelEventI, public NGCFT1EventI { class NGCHS2Send : public RegistryMessageModelEventI, public NGCFT1EventI {
@ -81,7 +58,7 @@ class NGCHS2Send : public RegistryMessageModelEventI, public NGCFT1EventI {
// comp on peer c // comp on peer c
// limit to 2 uploads per peer simultaniously // limit to 2 uploads per peer simultaniously
// TODO: increase for prod (4?) // TODO: increase for prod (4?) or maybe even lower?
// currently per type // currently per type
constexpr static size_t _max_parallel_per_peer {2}; constexpr static size_t _max_parallel_per_peer {2};
@ -100,8 +77,7 @@ class NGCHS2Send : public RegistryMessageModelEventI, public NGCFT1EventI {
float iterate(float delta); float iterate(float delta);
void handleRange(Contact3Handle c, const Events::NGCFT1_recv_request&); void handleTimeRange(Contact3Handle c, const Events::NGCFT1_recv_request&);
void handleSingleMessage(Contact3Handle c, const Events::NGCFT1_recv_request&);
protected: protected:
bool onEvent(const Message::Events::MessageConstruct&) override; bool onEvent(const Message::Events::MessageConstruct&) override;

View File

@ -1,34 +1,47 @@
# [NGC] Group-History-Sync (v2) [PoC] [Draft] # [NGC] Group-History-Sync (v2.1) [PoC] [Draft]
Simple group history sync that uses `peer public key` + `message_id` + `timestamp` (`ppk+mid+ts`) to, mostly, uniquely identify messages and deliver them. Simple group history sync that uses `timestamp` + `peer public key` + `message_id` (`ts+ppk+mid`) to, mostly, uniquely identify messages and deliver them.
Messages are bundled up in a `msgpack` `array` and sent as a file transfer.
## Requirements ## Requirements
TODO TODO: more?
### Msgpack
For serializing the messages.
### File transfers ### File transfers
For sending packs of messages. A single message can be larger than a single custom packet, so this is a must-have. For sending packs of messages.
Even a single message can be larger than a single custom packet, so this is a must-have.
This also allows for compression down the road.
## Procedure ## Procedure
Peer A can request `ppk+mid+ts` list for a given time range from peer B. Peer A can request `ts+ppk+mid+msg` list for a given time range from peer B.
Peer B then sends a filetransfer (with special file type) of list of `ppk+mid+ts`. Peer B then sends a filetransfer (with special file type) of list of `ts+ppk+mid+msg`.
Optionally compressed. (Delta-coding / zstd) Optionally compressed. (Delta-coding? / zstd)
Peer A keeps doing that until the desired time span is covered. Peer A keeps doing that until the desired time span is covered.
After that or simultaniously, Peer A requests messages from peer B, either indivitually, or packed? in ranges?.
Optionally compressed.
During all that, peer B usually does the same thing to peer A. During all that, peer B usually does the same thing to peer A.
TODO: deny request explicitly. also why (like perms and time range too large etc)
## Traffic savings ## Traffic savings
It is recomended to remember if a range has been requested and answered from a given peer, to reduce traffic. It is recomended to remember if a range has been requested and answered from a given peer, to reduce traffic.
While compression is optional, it is recommended. While compression is optional, it is recommended.
Timestamps fit delta coding.
Peer keys fit dicts.
Message ids are mostly high entropy.
The Message itself is text, so dict/huffman fits well.
TODO: store the 4 coloms SoA instead of AoS ?
## Message uniqueness ## Message uniqueness
@ -59,19 +72,41 @@ TODO: is reusing the ft request api a good idea for this?
| fttype | name | content (ft id) | | fttype | name | content (ft id) |
|------------|------|---------------------| |------------|------|---------------------|
| 0x00000f00 | time range | - ts start </br> - ts end </br> - supported compression? | | 0x00000f02 | time range msgpack | - ts start </br> - ts end |
| | TODO: id range based request? | |
| 0x00000f01 | single message | - ppk </br> - mid </br> - ts |
## File transfers ## File transfer content
| fttype | name | content | | fttype | name | content | note |
|------------|------|---------------------| |------------|------|----------------------------|---|
| 0x00000f00 | time range | - feature bitset (1byte? different compressions?) </br> - ts start </br> - ts end </br> - list size </br> \\+ entry `ppk` </br> \\+ entry `mid` </br> \\+ entry `ts` | | 0x00000f02 | time range msgpack | `message list` in msgpack | |
| 0x00000f01 | single message | - message type (text/textaction/file) </br> - text if text or action, file type and file id if file |
### time range msgpack
Msgpack array of messages.
```
name | type/size | note
-------------------------|-------------------|-----
- array | 32bit number msgs
- ts | 64bit deciseconds
- ppk | 32bytes
- mid | 16bit
- msgtype | enum (string or number?)
- if text/textaction |
- text | string | maybe byte array instead?
- if file |
- fkind | 32bit enum | is this right?
- fid | bytes kind | length depends on kind
```
Name is the actual string key.
Data type sizes are suggestions, if not defined by the tox protocol.
How unknown `msgtype`s are handled is client defined.
They can be fully ignored or displayed as broken.
## TODO ## TODO
- [ ] figure out a pro-active approach (instead of waiting for a range request) - [ ] figure out a pro-active approach (instead of waiting for a range request)
- [ ] compression in the ft layer? (would make it reusable) hint/autodetect? - [ ] compression in the ft layer? (would make it reusable) hint/autodetect/autoenable for >1k ?