-
Notifications
You must be signed in to change notification settings - Fork 906
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.