clockworklabs / clockworklabs/SpacetimeDB

Feature Request: `ScopedViewContext`

Open
#4,629 0 comments 5 reactions 0 assignees View on GitHub

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 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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.