Skip to content

Feature Request: ScopedViewContext #4629

@sean256

Description

@sean256

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 ViewContext even 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:

  1. Key resolver — runs per-caller to determine which scope group they belong to (lightweight lookup)
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions