@dxos/feed
Append-only replicatable log consisting of blocks.
Specification
Concept
The feed is an append-only replicated log where data is stored in immutable blocks. It is designed to run in multiple environments:
- Browser: Using
wasm-sqlitein a Client Services Worker. - Node: Native SQLite.
- Cloudflare Workers: Durable Objects SQL Storage.
All environments use effect-sql as the abstraction layer.
Capacity
- Feeds per Space: ~1000
- Blocks per Feed: ~10,000
Replication
- Push: Peers push blocks without positions.
- Pull: Peers pull blocks with positions, allowing them to reconstruct the global order locally.
Partial Replication
The protocol supports partial replication:
- Per-Feed Configuration: Each feed has independent replication settings.
- Default: No replication (None).
- Replica Mode:
replicate(fromPosition)-> Replicates[fromPosition, Infinity).fromPosition = 0: Full History.fromPosition = current: Live Tail only.fromPosition = now - 3days: Time-based window (Future potential).
- Metadata: Replication settings are stored alongside feed metadata (locally).
- Subset of Feeds: Clients can choose to replicate only a specific subset of feeds within a Space.
- TODO: Define mechanism for selecting which feeds to replicate.
- Range Selection: Replication is strictly defined from a specific start position to the end (future blocks).
- It is NOT possible to replicate only historical data without subscribing to new data.
- Logic:
replicate(feedId, fromPosition) -> [fromPosition, Infinity)
Server Architecture (Cloudflare)
- Initial Strategy: One Durable Object (DO) per Space.
- All feeds for a single space are managed by a single DO instance.
- Simplifies consistency and coordination.
- Scaling Strategy (Future): Sharding by Feed ID.
- Feeds can be distributed across multiple DOs.
- Shard Key: Prefix of
hash(feedId)(e.g., first 2 chars).
Ordering
Ordering is determined by the following rules:
- Positioned Blocks: Blocks that have been assigned a global
positionby the Server.- Ordered strictly by
position(ascending).
- Ordered strictly by
- Unpositioned Blocks: Blocks pending position assignment (locally created or in-flight).
- Ordered by Lamport Timestamp rules:
sequence(Ascending)actorId(Lexicographical)
- Ordered by Lamport Timestamp rules:
sequence: Assigned by the peer (Author). actorId + sequence forms the Lamport timestamp.
nextSeq = max(previous_seq_in_this_feed) + 1
position: Assigned ONLY by the Server. Monotonic counter per Space.
Idempotency
The protocol is idempotent. A block is uniquely identified by the tuple:
(spaceId, feedId, sequence, actorId)OR(spaceId, feedId, position)(if positioned)
Processing the same block multiple times must be handled gracefully (e.g., ON CONFLICT DO NOTHING).
Schema
Feeds Table (feeds)
Stores the mapping between global feed identifiers and local integer IDs.
| Field | Type | Description |
|---|---|---|
feedPrivateId |
number |
Primary Key. Local integer identifier for the feed (private to the peer). |
spaceId |
string |
The global Space ID (external). |
feedId |
string |
The global Feed ID (ULID). |
feedNamespace |
string |
Optional. Application specific namespace for filtering feeds. |
Indexes:
- Unique Index:
(spaceId, feedId)
Blocks Table (blocks)
| Field | Type | Description |
|---|---|---|
feedPrivateId |
number |
Foreign key to feeds.feedPrivateId. |
position |
number | null |
The global position index. null if unpositioned. |
sequence |
number |
Sequence Number (Assigned by Author). |
actorId |
string |
Actor ID (Public Key). |
prevSequence |
number |
Sequence Number part of the Predecessor ID. |
prevActorId |
string |
Actor ID (Public Key) part of the Predecessor ID. |
timestamp |
number |
Unix timestamp in milliseconds. |
data |
Uint8Array |
The content of the block. Immutable. |
Indexes:
- Unique Index:
(feedPrivateId, position) - Unique Index:
(feedPrivateId, sequence, actorId)
Subscriptions Table (subscriptions)
Stores active subscriptions to optimize wire protocol.
| Field | Type | Description |
|---|---|---|
subscriptionId |
string |
Primary Key. Unique ID. |
expiresAt |
number |
Unix timestamp when subscription expires. |
feedPrivateIds |
string |
JSON array of feedPrivateIds. |
Sync Protocol (Message-Based RPC)
The sync protocol is message-based. All messages include a requestId. Responses must copy the requestId from the request.
Query
- Input:
requestId,(feedIds: string[]) + cursorOR(subscriptionId: string) + cursor - Output:
requestId,Block[](Stream or Batch) - Description: Returns blocks from the specified feeds (or subscription) with
position > cursor.
- Input:
Subscribe
- Input:
requestId,feedIds: string[] - Output:
requestId,subscriptionId: string - Description: Registers a subscription for specific feeds. Returns a
subscriptionId.
- Input:
Append
- Input:
requestId,namespace?: string,blocks: Block[] - Output:
requestId,positions: number[] - Description: Appends blocks to their respective feeds.
- If
namespaceis provided and the feed is new, it sets the feed's namespace. - Returns the assigned global positions.
- If
- Input:
Logic
Feed Identification
A feed is globally identified by the tuple (spaceId, feedId).
spaceId: String provided externally.feedId: ULID string.
Timestamps
All timestamps (e.g., insertionTimestamp) are Unix timestamps in milliseconds.
Usage Examples
1. Append Blocks
Request:
{
"requestId": "req-1",
"namespace": "my-app-data",
"blocks": [
{
"actorId": "01H1...",
"sequence": 100,
"data": "...",
"timestamp": 1234567890
}
]
}
Response:
{
"requestId": "req-1",
"positions": [42]
}
2. Subscribe and Query
Subscribe Request:
{
"requestId": "req-2",
"feedIds": ["01H1..."]
}
Subscribe Response:
{
"requestId": "req-2",
"subscriptionId": "sub-123",
"expiresAt": 1234569999
}
3. List Feeds
Request:
{
"requestId": "req-4",
"namespace": "data"
}
Response:
{
"requestId": "req-4",
"feeds": [
{ "feedId": "01H1...", "namespace": "data" },
{ "feedId": "02H2...", "namespace": "data" }
]
}
Query Request (Poll using Subscription):
{
"requestId": "req-3",
"subscriptionId": "sub-123",
"cursor": 41
}
Query Response:
{
"requestId": "req-3",
"blocks": [
{
"position": 42,
"sequence": 100,
"actorId": "01H1...",
"data": "..."
}
]
}
TODOs
- Sync protocol only works if each namespace is synced separately.