Skip to content

Commit 3bc1bad

Browse files
API for mutable subscriptions
Closes #78. Includes rust and csharp examples.
1 parent 36cb01e commit 3bc1bad

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed

docs/subscriptions/index.md

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
# The SpacetimeDB Subscription API
2+
3+
The subscription API allows a client to replicate a subset of a database.
4+
It does so by registering SQL queries, which we call subscriptions, through a database connection.
5+
A client will only receive updates for rows that match the subscriptions it has registered.
6+
7+
This guide describes the two main interfaces that comprise the API - `SubscriptionBuilder` and `SubscriptionHandle`.
8+
By using these interfaces, you can create efficient and responsive client applications that only receive the data they need.
9+
10+
## SubscriptionBuilder
11+
12+
:::server-rust
13+
```rust
14+
pub struct SubscriptionBuilder<M: SpacetimeModule> { /* private fields */ }
15+
16+
impl<M: SpacetimeModule> SubscriptionBuilder<M> {
17+
/// Register a callback that runs when the subscription has been applied.
18+
/// This callback receives a context containing the current state of the subscription.
19+
pub fn on_applied(mut self, callback: impl FnOnce(&M::SubscriptionEventContext) + Send + 'static);
20+
21+
/// Register a callback to run when the subscription fails.
22+
///
23+
/// Note that this callback may run either when attempting to apply the subscription,
24+
/// in which case [`Self::on_applied`] will never run,
25+
/// or later during the subscription's lifetime if the module's interface changes,
26+
/// in which case [`Self::on_applied`] may have already run.
27+
pub fn on_error(mut self, callback: impl FnOnce(&M::ErrorContext, crate::Error) + Send + 'static);
28+
29+
/// Subscribe to a subset of database via a single SQL query.
30+
/// Returns a handle which you can use to monitor or cancel the subscription later.
31+
pub fn subscribe(self, query_sql: &str) -> M::SubscriptionHandle;
32+
33+
/// Subscribe to all rows from all tables.
34+
///
35+
/// This method is intended as a convenience
36+
/// for applications where client-side memory use and network bandwidth are not concerns.
37+
/// Applications where these resources are a constraint
38+
/// should register more precise queries via [`Self::subscribe`]
39+
/// in order to replicate only the subset of data which the client needs to function.
40+
///
41+
/// This method should not be combined with [`Self::subscribe`] on the same `DbConnection`.
42+
/// A connection may either [`Self::subscribe`] to particular queries,
43+
/// or [`Self::subscribe_to_all_tables`], but not both.
44+
/// Attempting to call [`Self::subscribe`]
45+
/// on a `DbConnection` that has previously used [`Self::subscribe_to_all_tables`],
46+
/// or vice versa, may misbehave in any number of ways,
47+
/// including dropping subscriptions, corrupting the client cache, or panicking.
48+
pub fn subscribe_to_all_tables(self);
49+
}
50+
```
51+
:::
52+
:::server-csharp
53+
```cs
54+
public sealed class SubscriptionBuilder<SubscriptionEventContext, ErrorContext>
55+
where SubscriptionEventContext : ISubscriptionEventContext
56+
where ErrorContext : IErrorContext
57+
{
58+
/// <summary>
59+
/// Register a callback that runs when the subscription has been applied.
60+
/// This callback receives a context containing the current state of the subscription.
61+
/// </summary>
62+
public SubscriptionBuilder<SubscriptionEventContext, ErrorContext> OnApplied(
63+
Action<SubscriptionEventContext> callback
64+
);
65+
66+
/// <summary>
67+
/// Register a callback to run when the subscription fails.
68+
/// </summary>
69+
public SubscriptionBuilder<SubscriptionEventContext, ErrorContext> OnError(
70+
Action<ErrorContext, Exception> callback
71+
);
72+
73+
/// <summary>
74+
/// Subscribe to a subset of database via a single SQL query.
75+
/// Returns a handle which you can use to monitor or cancel the subscription later.
76+
/// </summary>
77+
public SubscriptionHandle<SubscriptionEventContext, ErrorContext> Subscribe(
78+
string querySql
79+
);
80+
81+
/// <summary>
82+
/// Subscribe to all rows from all tables.
83+
///
84+
/// This method is intended as a convenience
85+
/// for applications where client-side memory use and network bandwidth are not concerns.
86+
/// Applications where these resources are a constraint
87+
/// should register more precise queries via [`Subscribe`]
88+
/// in order to replicate only the subset of data which the client needs to function.
89+
///
90+
/// This method should not be combined with `Subscribe` on the same `DbConnection`.
91+
/// A connection may either `Subscribe` to particular queries,
92+
/// or `SubscribeToAllTables`, but not both.
93+
/// Attempting to call `Subscribe`
94+
/// on a `DbConnection` that has previously used `SubscribeToAllTables`,
95+
/// or vice versa, may misbehave in any number of ways,
96+
/// including dropping subscriptions or corrupting the client cache.
97+
/// </summary>
98+
public void SubscribeToAllTables();
99+
}
100+
```
101+
:::
102+
103+
A `SubscriptionBuilder` provides an interface for registering subscription queries with a database.
104+
It allows you to register callbacks that run when the subscription is successfully applied or when an error occurs.
105+
Once applied, a client will start receiving row updates to its client cache.
106+
A client can react to these updates by registering row callbacks for the appropriate table.
107+
108+
It is important to note that subscriptions must be disjoint from one another.
109+
Two subscriptions that return the same row may result in a corrupted client cache.
110+
That is, one that does not accurately and consistently reflect the state of the database.
111+
112+
### Example Usage
113+
114+
:::server-rust
115+
```rust
116+
// Establish a database connection
117+
let conn: DbConnection = connect_to_db();
118+
119+
// Register a subscription with the database
120+
let subscription_handle = conn
121+
.subscription_builder()
122+
.on_applied(|ctx| { /* handle applied state */ })
123+
.on_error(|error_ctx, error| { /* handle error */ })
124+
.subscribe("SELECT * FROM my_table WHERE active = 1");
125+
```
126+
:::
127+
:::server-csharp
128+
```cs
129+
// Establish a database connection
130+
var conn = ConnectToDB();
131+
132+
// Register a subscription with the database
133+
var userSubscription = conn
134+
.SubscriptionBuilder()
135+
.OnApplied((ctx) => { /* handle applied state */ })
136+
.OnError((errorCtx, error) => { /* handle error */ })
137+
.Subscribe("SELECT * FROM user");
138+
```
139+
:::
140+
141+
## SubscriptionHandle
142+
143+
:::server-rust
144+
```rust
145+
pub trait SubscriptionHandle: InModule + Clone + Send + 'static
146+
where
147+
Self::Module: SpacetimeModule<SubscriptionHandle = Self>,
148+
{
149+
/// Returns `true` if the subscription has been ended.
150+
/// That is, if it has been unsubscribed or terminated due to an error.
151+
fn is_ended(&self) -> bool;
152+
153+
/// Returns `true` if the subscription is currently active.
154+
fn is_active(&self) -> bool;
155+
156+
/// Unsubscribe from the query controlled by this `SubscriptionHandle`,
157+
/// then run `on_end` when its rows are removed from the client cache.
158+
/// Returns an error if the subscription is already ended,
159+
/// or if unsubscribe has already been called.
160+
fn unsubscribe_then(self, on_end: OnEndedCallback<Self::Module>) -> crate::Result<()>;
161+
162+
/// Unsubscribe from the query controlled by this `SubscriptionHandle`.
163+
/// Returns an error if the subscription is already ended,
164+
/// or if unsubscribe has already been called.
165+
fn unsubscribe(self) -> crate::Result<()>;
166+
}
167+
```
168+
:::
169+
:::server-csharp
170+
```cs
171+
public class SubscriptionHandle<SubscriptionEventContext, ErrorContext> : ISubscriptionHandle
172+
where SubscriptionEventContext : ISubscriptionEventContext
173+
where ErrorContext : IErrorContext
174+
{
175+
/// <summary>
176+
/// Whether the subscription has ended.
177+
/// </summary>
178+
public bool IsEnded;
179+
180+
/// <summary>
181+
/// Whether the subscription is active.
182+
/// </summary>
183+
public bool IsActive;
184+
185+
/// <summary>
186+
/// Unsubscribe from the query controlled by this subscription handle.
187+
///
188+
/// Calling this more than once will result in an exception.
189+
/// </summary>
190+
public void Unsubscribe();
191+
192+
/// <summary>
193+
/// Unsubscribe from the query controlled by this subscription handle,
194+
/// and call onEnded when its rows are removed from the client cache.
195+
/// </summary>
196+
public void UnsubscribeThen(Action<SubscriptionEventContext>? onEnded);
197+
}
198+
```
199+
:::
200+
201+
When you register a subscription, you receive a `SubscriptionHandle`.
202+
A `SubscriptionHandle` manages the lifecycle of each subscription you register.
203+
In particular, it provides methods to check the status of the subscription and to unsubscribe if necessary.
204+
Because each subscription has its own independently managed lifetime,
205+
clients can dynamically subscribe to different subsets of the database as their application requires.
206+
207+
### Example Usage
208+
209+
:::server-rust
210+
Consider a game client that displays shop items based on a player's level.
211+
You subscribe to the following `shop_items` when a player is at level 5.
212+
213+
```rust
214+
let conn: DbConnection = connect_to_db();
215+
216+
let shop_items_subscription = conn
217+
.subscription_builder()
218+
.on_applied(|ctx| { /* handle applied state */ })
219+
.on_error(|error_ctx, error| { /* handle error */ })
220+
.subscribe("SELECT * FROM shop_items WHERE required_level <= 5");
221+
```
222+
223+
Later, when the player reaches level 6 and new items become available,
224+
you unsubscribe from the old query and subscribe again with a new one:
225+
226+
```rust
227+
if shop_items_subscription.is_active() {
228+
shop_items_subscription
229+
.unsubscribe()
230+
.expect("Unsubscribing from shop_items failed");
231+
}
232+
233+
let shop_items_subscription = conn
234+
.subscription_builder()
235+
.on_applied(|ctx| { /* handle applied state */ })
236+
.on_error(|error_ctx, error| { /* handle error */ })
237+
.subscribe("SELECT * FROM shop_items WHERE required_level <= 6");
238+
```
239+
240+
All other subscriptions continue to remain in effect.
241+
:::
242+
:::server-csharp
243+
Consider a game client that displays shop items based on a player's level.
244+
You subscribe to the following `shop_items` when a player is at level 5.
245+
246+
```cs
247+
var conn = ConnectToDB();
248+
249+
var shopItemsSubscription = conn
250+
.SubscriptionBuilder()
251+
.OnApplied((ctx) => { /* handle applied state */ })
252+
.OnError((errorCtx, error) => { /* handle error */ })
253+
.Subscribe("SELECT * FROM shop_items WHERE required_level <= 5");
254+
```
255+
256+
Later, when the player reaches level 6 and new items become available,
257+
you unsubscribe from the old query and subscribe again with a new one:
258+
259+
```cs
260+
if (shopItemsSubscription.IsActive)
261+
{
262+
shopItemsSubscription.Unsubscribe();
263+
}
264+
265+
var shopItemsSubscription = conn
266+
.SubscriptionBuilder()
267+
.OnApplied((ctx) => { /* handle applied state */ })
268+
.OnError((errorCtx, error) => { /* handle error */ })
269+
.Subscribe("SELECT * FROM shop_items WHERE required_level <= 6");
270+
```
271+
272+
All other subscriptions continue to remain in effect.
273+
:::

0 commit comments

Comments
 (0)