Initial commit
This commit is contained in:
Binary file not shown.
+298
@@ -0,0 +1,298 @@
|
||||
<p align="center">
|
||||
<img width="300" src=".github/logo.png"/>
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://github.com/supabase/realtime-csharp/workflows/Build%20And%20Test/badge.svg"/>
|
||||
<a href="https://www.nuget.org/packages/realtime-csharp/">
|
||||
<img src="https://img.shields.io/badge/dynamic/json?color=green&label=Nuget%20Release&query=data[0].version&url=https%3A%2F%2Fazuresearch-usnc.nuget.org%2Fquery%3Fq%3Dpackageid%3Arealtime-csharp"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## BREAKING CHANGES MOVING FROM v5.x.x to v6.x.x
|
||||
|
||||
- The realtime client now takes a "fail-fast" approach. On establishing an initial connection, client will throw
|
||||
a `RealtimeException` in `ConnectAsync()` if the socket server is unreachable. After an initial connection has been
|
||||
established, the **client will continue attempting reconnections indefinitely until disconnected.**
|
||||
- [Major, New] C# `EventHandlers` have been changed to `delegates`. This should allow for cleaner event data access over
|
||||
the previous subclassed `EventArgs` setup. Events are scoped accordingly. For example, the `RealtimeSocket` error
|
||||
handlers will receive events regarding socket connectivity; whereas the `RealtimeChannel` error handlers will receive
|
||||
events according to `Channel` joining/leaving/etc. This is implemented with the following methods prefixed by (
|
||||
Add/Remove/Clear):
|
||||
- `RealtimeBroadcast.AddBroadcastEventHandler`
|
||||
- `RealtimePresence.AddPresenceEventHandler`
|
||||
- `RealtimeSocket.AddStateChangedHandler`
|
||||
- `RealtimeSocket.AddMessageReceivedHandler`
|
||||
- `RealtimeSocket.AddHeartbeatHandler`
|
||||
- `RealtimeSocket.AddErrorHandler`
|
||||
- `RealtimeClient.AddDebugHandler`
|
||||
- `RealtimeClient.AddStateChangedHandler`
|
||||
- `RealtimeChannel.AddPostgresChangeHandler`
|
||||
- `RealtimeChannel.AddMessageReceivedHandler`
|
||||
- `RealtimeChannel.AddErrorHandler`
|
||||
- `Push.AddMessageReceivedHandler`
|
||||
- [Major, new] `ClientOptions.Logger` has been removed in favor of `Client.AddDebugHandler()` which allows for
|
||||
implementing custom logging solutions if desired.
|
||||
- A simple logger can be set up with the following:
|
||||
```c#
|
||||
client.AddDebugHandler((sender, message, exception) => Debug.WriteLine(message));
|
||||
```
|
||||
- [Major] `Connect()` has been marked `Obsolete` in favor of `ConnectAsync()`
|
||||
- Custom reconnection logic has been removed in favor of using the built-in logic from `Websocket.Client@4.6.1`.
|
||||
- Exceptions that are handled within this library have been marked as `RealtimeException`s.
|
||||
- The local, docker-composed test suite has been brought back (as opposed to remotely testing on live supabase servers)
|
||||
to test against.
|
||||
- Comments have been added throughout the entire codebase and an `XML` file is now generated on build.
|
||||
|
||||
---
|
||||
|
||||
**See realtime-csharp in action [here](https://multiplayer-csharp.azurewebsites.net/).**
|
||||
|
||||
`realtime-csharp` is written as a client library for [supabase/realtime](https://github.com/supabase/realtime).
|
||||
|
||||
Documentation can be
|
||||
found [here](https://supabase-community.github.io/realtime-csharp/api/Supabase.Realtime.Client.html).
|
||||
|
||||
The bulk of this library is a translation and c-sharp-ification of
|
||||
the [supabase/realtime-js](https://github.com/supabase/realtime-js) library.
|
||||
|
||||
**The Websocket-sharp implementation that Realtime-csharp is dependent on does _not_ support TLS1.3**
|
||||
|
||||
## Getting Started
|
||||
|
||||
Care was had to make this API as _easy<sup>tm</sup>_ to interact with as possible. `Connect()` and `Subscribe()`
|
||||
have `await`-able signatures
|
||||
which allow Users to be assured that a connection exists prior to interacting with it.
|
||||
|
||||
```c#
|
||||
var endpoint = "ws://realtime-dev.localhost:4000/socket";
|
||||
client = new Client(endpoint);
|
||||
|
||||
await client.ConnectAsync();
|
||||
|
||||
// Shorthand for registering a postgres_changes subscription
|
||||
var channel = client.Channel("realtime", "public", "todos");
|
||||
|
||||
// Listen to Updates
|
||||
channel.AddPostgresChangeHandler(ListenType.Updates, (_, change) =>
|
||||
{
|
||||
var model = change.Model<Todo>();
|
||||
var oldModel = change.OldModel<Todo>();
|
||||
});
|
||||
await channel.Subscribe();
|
||||
```
|
||||
|
||||
Leveraging `Postgrest.BaseModel`s, one ought to be able to coerce SocketResponse Records into their associated models by
|
||||
calling:
|
||||
|
||||
```c#
|
||||
// ...
|
||||
var channel = client.Channel("realtime", "public", "users");
|
||||
|
||||
channel.AddPostgresChangeHandler(ListenType.Inserts, (_, change) =>
|
||||
{
|
||||
var model = change.Model<Todo>();
|
||||
});
|
||||
|
||||
await channel.Subscribe();
|
||||
```
|
||||
|
||||
## Broadcast
|
||||
|
||||
"Broadcast follows the publish-subscribe pattern where a client publishes messages to a channel with a unique
|
||||
identifier. For example, a user could send a message to a channel with id room-1.
|
||||
|
||||
Other clients can elect to receive the message in real-time by subscribing to the channel with id room-1. If these
|
||||
clients are online and subscribed then they will receive the message.
|
||||
|
||||
Broadcast works by connecting your client to the nearest Realtime server, which will communicate with other servers to
|
||||
relay messages to other clients.
|
||||
|
||||
A common use-case is sharing a user's cursor position with other clients in an online game."
|
||||
|
||||
[Find more information here](https://supabase.com/docs/guides/realtime#broadcast)
|
||||
|
||||
**Given the following model (`CursorBroadcast`):**
|
||||
|
||||
```c#
|
||||
class MouseBroadcast : BaseBroadcast<MouseStatus> { }
|
||||
class MouseStatus
|
||||
{
|
||||
[JsonProperty("mouseX")]
|
||||
public float MouseX { get; set; }
|
||||
|
||||
[JsonProperty("mouseY")]
|
||||
public float MouseY { get; set; }
|
||||
|
||||
[JsonProperty("userId")]
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Listen for typed broadcast events**:
|
||||
|
||||
```c#
|
||||
var channel = supabase.Realtime.Channel("cursor");
|
||||
|
||||
var broadcast = channel.Register<MouseBroadcast>(false, true);
|
||||
broadcast.AddBroadcastEventHandler((sender, _) =>
|
||||
{
|
||||
// Retrieved typed model.
|
||||
var state = broadcast.Current();
|
||||
|
||||
Debug.WriteLine($"{state.Payload}: {state.Payload.MouseX}:{state.Payload.MouseY}");
|
||||
});
|
||||
await channel.Subscribe();
|
||||
```
|
||||
|
||||
**Broadcast an event**:
|
||||
|
||||
```c#
|
||||
var channel = supabase.Realtime.Channel("cursor");
|
||||
var data = new CursorBroadcast { Event = "cursor", Payload = new MouseStatus { MouseX = 123, MouseY = 456 } };
|
||||
channel.Send(ChannelType.Broadcast, data);
|
||||
```
|
||||
|
||||
## Presence
|
||||
|
||||
"Presence utilizes an in-memory conflict-free replicated data type (CRDT) to track and synchronize shared state in an
|
||||
eventually consistent manner. It computes the difference between existing state and new state changes and sends the
|
||||
necessary updates to clients via Broadcast.
|
||||
|
||||
When a new client subscribes to a channel, it will immediately receive the channel's latest state in a single message
|
||||
instead of waiting for all other clients to send their individual states.
|
||||
|
||||
Clients are free to come-and-go as they please, and as long as they are all subscribed to the same channel then they
|
||||
will all have the same Presence state as each other.
|
||||
|
||||
The neat thing about Presence is that if a client is suddenly disconnected (for example, they go offline), their state
|
||||
will be automatically removed from the shared state. If you've ever tried to build an “I'm online” feature which handles
|
||||
unexpected disconnects, you'll appreciate how useful this is."
|
||||
|
||||
[Find more information here](https://supabase.com/docs/guides/realtime#presence)
|
||||
|
||||
**Given the following model: (`UserPresence`)**
|
||||
|
||||
```c#
|
||||
class UserPresence: BasePresence
|
||||
{
|
||||
[JsonProperty("lastSeen")]
|
||||
public DateTime LastSeen { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Listen for typed presence events**:
|
||||
|
||||
```c#
|
||||
var presenceId = Guid.NewGuid().ToString();
|
||||
|
||||
var channel = supabase.Realtime.Channel("last-seen");
|
||||
var presence = channel.Register<UserPresence>(presenceId);
|
||||
|
||||
presence.AddPresenceEventHandler(EventType.Sync, (sender, type) =>
|
||||
{
|
||||
foreach (var state in presence.CurrentState)
|
||||
{
|
||||
var userId = state.Key;
|
||||
var lastSeen = state.Value.First().LastSeen;
|
||||
Debug.WriteLine($"{userId}: {lastSeen}");
|
||||
}
|
||||
});
|
||||
await channel.Subscribe();
|
||||
```
|
||||
|
||||
**Track a user presence event**:
|
||||
|
||||
```c#
|
||||
var presenceId = Guid.NewGuid().ToString();
|
||||
var channel = supabase.Realtime.Channel("last-seen");
|
||||
|
||||
var presence = channel.Register<UserPresence>(presenceId);
|
||||
presence.Track(new UserPresence { LastSeen = DateTime.Now });
|
||||
```
|
||||
|
||||
## Postgres Changes
|
||||
|
||||
"Postgres Changes enable you to listen to database changes and have them broadcast to authorized clients based
|
||||
on [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security) policies.
|
||||
|
||||
This works by Realtime polling your database's logical replication slot for changes, passing those changes to
|
||||
the [apply_rls](https://github.com/supabase/walrus#reading-wal) SQL function to determine which clients have permission,
|
||||
and then using Broadcast to send those changes to clients.
|
||||
|
||||
Realtime requires a publication called `supabase_realtime` to determine which tables to poll. You must add tables to
|
||||
this publication prior to clients subscribing to channels that want to listen for database changes.
|
||||
|
||||
We strongly encourage you to enable RLS on your database tables and have RLS policies in place to prevent unauthorized
|
||||
parties from accessing your data."
|
||||
|
||||
[Find More Information here](https://supabase.com/docs/guides/realtime#postgres-changes)
|
||||
|
||||
**Using the new `Register` method:**
|
||||
|
||||
```c#
|
||||
var channel = supabase.Realtime.Channel("public-users");
|
||||
channel.Register(new PostgresChangesOptions("public", "users"));
|
||||
channel.AddPostgresChangeHandler(ListenType.All, (sender, change) =>
|
||||
{
|
||||
switch (change.Event)
|
||||
{
|
||||
case EventType.Insert:
|
||||
// User has been created
|
||||
break;
|
||||
case EventType.Update:
|
||||
// User has been updated
|
||||
break;
|
||||
case EventType.Delete:
|
||||
// User has been deleted
|
||||
break;
|
||||
}
|
||||
});
|
||||
await channel.Subscribe();
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
- [x] Client Connects to Websocket
|
||||
- [x] Socket Event Handlers
|
||||
- [x] Open
|
||||
- [x] Close - when channel is explicitly closed by server or by calling `Channel.Unsubscribe()`
|
||||
- [x] Error
|
||||
- [x] Realtime Event Handlers
|
||||
- [x] `INSERT`
|
||||
- [x] `UPDATE`
|
||||
- [x] `DELETE`
|
||||
- [x] `*`
|
||||
- [x] Join channels of format:
|
||||
- [x] `{database}`
|
||||
- [x] `{database}:{schema}`
|
||||
- [x] `{database}:{schema}:{table}`
|
||||
- [x] `{database}:{schema}:{table}:{col}.eq.{val}`
|
||||
- [x] Responses supply a Generically Typed Model derived from `BaseModel`
|
||||
- [x] Ability to remove subscription to Realtime Events
|
||||
- [x] Ability to disconnect from socket.
|
||||
- [x] Socket reconnects when possible
|
||||
- [x] Unit Tests
|
||||
- [x] Documentation
|
||||
- [x] Nuget Release
|
||||
|
||||
## Package made possible through the efforts of:
|
||||
|
||||
Join the ranks! See a problem? Help fix it!
|
||||
|
||||
<a href="https://github.com/supabase-community/realtime-csharp/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=supabase-community/realtime-csharp" />
|
||||
</a>
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks/preview?repo=supabase-community%2Frealtime-csharp).
|
||||
|
||||
## Contributing
|
||||
|
||||
We are more than happy to have contributions! Please submit a PR.
|
||||
|
||||
## Testing
|
||||
|
||||
Note that the latest versions of `supabase/realtime` expect to be able to access a subdomain matching the tenant. For
|
||||
the case of testing, this means that `realtime-dev.localhost:4000` should be available. To have tests run locally,
|
||||
please add a hosts entry on your system for: `127.0.0.1 realtime-dev.localhost`
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed94082386d0cf147a0ed54b98e06ace
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5521a79b4acd1013574150313315a0ec9093eca0072a4b1f6052ac1ef0c988c9
|
||||
size 5771
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eaf9f3b36ba5efe4b9b71b5ba728b550
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27115e0264b3f5f44a2694e938775769
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe1d772f2816c58418931c899fd5cf2d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57712420560676d44a2e29cee4e07d37
|
||||
labels:
|
||||
- NuGetForUnity
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 3
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
Any:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
Editor:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
WindowsStoreApps:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+2676
File diff suppressed because it is too large
Load Diff
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d01ffd3ebd3fc794a9f9532181016c7b
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>realtime-csharp</id>
|
||||
<version>6.0.4</version>
|
||||
<title>realtime-csharp</title>
|
||||
<authors>Joseph Schultz <joseph@acupofjose.com></authors>
|
||||
<license type="expression">MIT</license>
|
||||
<licenseUrl>https://licenses.nuget.org/MIT</licenseUrl>
|
||||
<icon>icon.png</icon>
|
||||
<readme>README.md</readme>
|
||||
<projectUrl>https://github.com/supabase/realtime-csharp</projectUrl>
|
||||
<description>Realtime-csharp is written as a client library for supabase/realtime.</description>
|
||||
<copyright>MIT</copyright>
|
||||
<tags>supabase, realtime, phoenix</tags>
|
||||
<repository url="https://github.com/supabase/realtime-csharp" />
|
||||
<dependencies>
|
||||
<group targetFramework=".NETStandard2.0">
|
||||
<dependency id="Newtonsoft.Json" version="13.0.3" exclude="Build,Analyzers" />
|
||||
<dependency id="Websocket.Client" version="4.6.1" exclude="Build,Analyzers" />
|
||||
<dependency id="postgrest-csharp" version="3.2.2" exclude="Build,Analyzers" />
|
||||
<dependency id="supabase-core" version="0.0.3" exclude="Build,Analyzers" />
|
||||
</group>
|
||||
</dependencies>
|
||||
</metadata>
|
||||
</package>
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50f357ca3478f554e9dab8b132e33c2a
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user