clockworklabs / clockworklabs/SpacetimeDB
Feature Request: `ScopedViewContext`
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 25.2k
- Forks
- 1.1k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 46
Description
Summary
Introduce a new view context type, ScopedViewContext<K>, that allows view results to be scoped and shared by an arbitrary key rather than being either global (AnonymousViewContext) or per-caller (ViewContext).
Motivation
AnonymousViewContext offers a significant performance advantage — the database materializes the view once and shares it across all subscribers. The only alternative today is ViewContext, which is tied to the individual caller and requires a separate computation per subscriber.
There is a large gap between these two extremes. Many real-world use cases involve groups of users who should all see the same view results. Today, module authors must work around this by either:
- Manually partitioning data into hardcoded anonymous views (e.g., one view per known region) (not acceptable for sensitive data)
- Accepting the per-user cost of
ViewContexteven when many users would share identical results
A ScopedViewContext closes this gap by letting SpacetimeDB automatically memoize and share view computations across all callers that resolve to the same scope key.
Proposed API
The API uses two callbacks:
- Key resolver — runs per-caller to determine which scope group they belong to (lightweight lookup)
- View body — runs once per unique key, shared across all callers with that key
TypeScript
// Scope key as a single value
export const team_chat = spacetimedb.scopedView(
{ name: 'team_chat', public: true },
t.array(chatMessages.rowType),
// 1. Resolve the scope key for this caller
(ctx) => {
const player = ctx.db.players.identity.find(ctx.sender());
return player.teamId; // u64 key — all players on the same team share this view
},
// 2. View body — executed once per unique teamId
(ctx, teamId) => {
return Array.from(ctx.db.chatMessages.teamId.filter(teamId));
}
);
// Scope key as a composite struct/tuple
export const regional_entities = spacetimedb.scopedView(
{ name: 'regional_entities', public: true },
t.array(entity.rowType),
(ctx) => {
const player = ctx.db.players.identity.find(ctx.sender());
const chunk = ctx.db.playerChunks.playerId.find(player.id);
return { chunkX: chunk.chunkX, chunkY: chunk.chunkY }; // composite key
},
(ctx, key) => {
return Array.from(ctx.db.entity.chunkX.filter(key.chunkX))
.filter(e => e.chunkY === key.chunkY);
}
);
Rust
// Scope key as a single value
#[view(accessor = team_chat, public)]
fn team_chat(ctx: &ScopedViewContext<u64>) -> Vec<ChatMessage> {
ctx.db.chat_messages().team_id().filter(ctx.scope()).collect()
}
#[view_scope(team_chat)]
fn team_chat_scope(ctx: &ViewContext) -> u64 {
let player = ctx.db.player().identity().find(&ctx.sender()).unwrap();
player.team_id
}
// Scope key as a composite struct
#[derive(ScopeKey)]
struct ChunkCoord {
chunk_x: i32,
chunk_y: i32,
}
#[view(accessor = regional_entities, public)]
fn regional_entities(ctx: &ScopedViewContext<ChunkCoord>) -> Vec<Entity> {
let key = ctx.scope();
ctx.db.entity().chunk_x().filter(&key.chunk_x)
.filter(|e| e.chunk_y == key.chunk_y)
.collect()
}
#[view_scope(regional_entities)]
fn regional_entities_scope(ctx: &ViewContext) -> ChunkCoord {
let player = ctx.db.player().identity().find(&ctx.sender()).unwrap();
let chunk = ctx.db.player_chunk().player_id().find(&player.id).unwrap();
ChunkCoord { chunk_x: chunk.chunk_x, chunk_y: chunk.chunk_y }
}
Valid Scope Key Types
Scope keys must be hashable and comparable. Valid types include:
- Primitives:
u64,string,Identity - Composite keys: Structs or tuples composed of the above types (e.g.,
{ chunkX: i32, chunkY: i32 })
Example Use Cases
| Use Case | Scope Key | Why |
|---|---|---|
| Team chat | teamId: u64 |
All players on the same team see the same messages |
| Match leaderboard | matchId: u64 |
All players in a match share the same leaderboard |
| Regional map data | { chunkX, chunkY } |
Players in the same chunk share entity data |
| Guild inventory | guildId: u64 |
All guild members see the same shared inventory |
| Instance/room state | instanceId: u64 |
Players in the same dungeon instance share state |
| Party buffs | partyId: u64 |
Active buffs shared across party members |
Performance Characteristics
| Context Type | Computation Cost | Materialized Views |
|---|---|---|
AnonymousViewContext |
Once total | 1 |
ScopedViewContext<K> |
Once per unique key | 1 per unique key |
ViewContext |
Once per subscriber | 1 per subscriber |
For a game with 1,000 players across 50 teams, team-scoped views would require ~50 materializations instead of 1,000 — a 20x reduction compared to ViewContext.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the existing AnonymousViewContext and ViewContext implementations, along with the Rust and TypeScript view registration paths. Compare their lifecycle, callback, and materialization behavior with the proposed ScopedViewContext API. Done means scoped views support primitive and composite keys, share results per key, and have coverage for the documented examples.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, typescript
- Domain
- backend-api-design, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100