Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/in-depth/client/advanced/blazor-wasm.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ You can use Blazor WASM with the Datasync Community Toolkit; however, there are

It is possible to use offline mode by storing the SQLite database in local storage within the browser. When you start up your application, you restore the SQLite in-memory database into memory; on a regular basis, you write the SQLite database to local storage. However, this system severely affects performance within the browser and large synchronizations will cause the local storage to overflow. As a result of these problems, it is not recommended, nor is it supported.

!!! warning "Known limitations of offline mode on browser-wasm"
If you do experiment with offline mode in the browser, be aware of the following single-threaded browser-wasm limitations. These are not currently fixed because offline mode in Blazor WASM is not supported.

* **Use the async APIs.** The synchronous `SaveChanges()` calls `OperationsQueueManager.UpdateOperationsQueue()`, which blocks on a thread-pool task and cannot work on single-threaded WASM. Use `SaveChangesAsync()` and the `PullAsync()` / `PushAsync()` methods instead.
* **Specify a query ID for filtered pulls.** When a pull query has an OData filter and you do not supply a query ID, the toolkit generates one using `MD5`, which is marked as unsupported on `browser`. Pass an explicit query ID to avoid this.

Blocking waits on `SemaphoreSlim` (which throw on .NET 11 browser-wasm) are no longer used when building the offline options, so the first `PullAsync()` call no longer fails for that reason.

## Suppress the `WASM0001` warning

When compiling the WASM client, you will see warning `WASM0001`. This is a harmless warning that indicates SQLite is not available. However, you may be running using "Warnings as Errors". The most appropriate solution is to suppress the `WASM0001` warning in your client `.csproj` file as follows:
Expand Down
24 changes: 15 additions & 9 deletions src/CommunityToolkit.Datasync.Client/Offline/OfflineDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ public abstract partial class OfflineDbContext : DbContext
/// A checker for the disposed state of this context.
/// </summary>
internal bool _disposedValue;
internal DisposableLock _optionsLock = new();

/// <summary>
/// Guards the lazy creation of <see cref="OfflineOptions"/>. A monitor-based <see cref="Lock"/> is used instead of
/// a <see cref="SemaphoreSlim"/> because blocking semaphore waits are not supported on single-threaded browser-wasm.
/// </summary>
private readonly Lock _optionsLock = new();

/// <summary>
/// The operations queue. This is used to store pending requests to the datasync service.
Expand Down Expand Up @@ -144,15 +149,17 @@ protected OfflineDbContext(DbContextOptions options) : base(options)
/// <returns>The offline options for the datasync operation.</returns>
internal OfflineOptions BuildDatasyncOfflineOptions()
{
using IDisposable optionsLock = this._optionsLock.AcquireLock();
if (OfflineOptions is null)
lock (this._optionsLock)
{
DatasyncOfflineOptionsBuilder builder = new(QueueManager.GetSynchronizableEntityTypes());
OnDatasyncInitialization(builder);
OfflineOptions = builder.Build();
if (OfflineOptions is null)
{
DatasyncOfflineOptionsBuilder builder = new(QueueManager.GetSynchronizableEntityTypes());
OnDatasyncInitialization(builder);
OfflineOptions = builder.Build();
}

return OfflineOptions;
}

return OfflineOptions;
}

/// <summary>
Expand Down Expand Up @@ -604,7 +611,6 @@ protected virtual void Dispose(bool disposing)
{
if (disposing)
{
this._optionsLock.Dispose();
base.Dispose();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,6 @@ public async Task<IDisposable> AcquireLockAsync(string key, CancellationToken ca
});
}

/// <summary>
/// Acquire a lock (asynchronously) from the dictionary of locks.
/// </summary>
/// <param name="key">The key for the lock.</param>
/// <returns>An <see cref="IDisposable"/> to release the lock when done.</returns>
public IDisposable AcquireLock(string key)
{
LockEntry entry = this.locks.GetOrAdd(key, _ => new LockEntry());
_ = entry.IncrementCount();

IDisposable releaser = entry.Lock.AcquireLock();
return new DisposeAction(() =>
{
releaser.Dispose();
if (entry.DecrementCount() == 0)
{
_ = this.locks.TryRemove(key, out _);
entry.Dispose();
}
});
}

/// <summary>
/// Determines if the specific lock referenced by key is locked now.
/// </summary>
Expand Down
10 changes: 0 additions & 10 deletions src/CommunityToolkit.Datasync.Client/Threading/DisposableLock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@ internal class DisposableLock : IDisposable
/// </summary>
private readonly SemaphoreSlim semaphore = new(1, 1);

/// <summary>
/// Acquire the lock.
/// </summary>
/// <returns>An <see cref="IDisposable"/> to release the lock when the lock is acquired.</returns>
public IDisposable AcquireLock()
{
this.semaphore.Wait();
return new DisposeAction(() => this.semaphore.Release());
}

/// <summary>
/// Acquire the lock.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#pragma warning disable IDE0305 // Simplify collection initialization

using CommunityToolkit.Datasync.Client.Offline;
using CommunityToolkit.Datasync.Client.Offline.Models;
using CommunityToolkit.Datasync.Client.Serialization;
using CommunityToolkit.Datasync.Client.Test.Offline.Helpers;
using CommunityToolkit.Datasync.TestCommon;
Expand Down Expand Up @@ -41,6 +42,34 @@ public void Base_Ctor_CreatesInternalApi()
}
#endregion

#region BuildDatasyncOfflineOptions
[Fact]
public void BuildDatasyncOfflineOptions_ReturnsSameInstance()
{
int configured = 0;
this.context.Configurator = _ => configured++;

OfflineOptions first = this.context.BuildDatasyncOfflineOptions();
OfflineOptions second = this.context.BuildDatasyncOfflineOptions();

second.Should().BeSameAs(first);
configured.Should().Be(1);
}

[Fact]
public async Task BuildDatasyncOfflineOptions_ConcurrentCalls_BuildOnce()
{
int configured = 0;
this.context.Configurator = _ => Interlocked.Increment(ref configured);

OfflineOptions[] results = await Task.WhenAll(
Enumerable.Range(0, 16).Select(_ => Task.Run(() => this.context.BuildDatasyncOfflineOptions())));

results.Should().OnlyContain(o => ReferenceEquals(o, results[0]));
configured.Should().Be(1);
}
#endregion

#region PullAsync
[Fact]
public async Task PullAsync_List_Works_InitialSync()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,6 @@ namespace CommunityToolkit.Datasync.Client.Test.Threading;
[ExcludeFromCodeCoverage]
public class AsyncLockDictionary_Tests
{
[Fact]
public void AcquireLock_Works()
{
AsyncLockDictionary sut = new();

sut.IsLocked("key1").Should().BeFalse();
sut.IsLocked("key2").Should().BeFalse();

using (sut.AcquireLock("key1"))
{
sut.IsLocked("key1").Should().BeTrue();
sut.IsLocked("key2").Should().BeFalse();
}

sut.IsLocked("key1").Should().BeFalse();
sut.IsLocked("key2").Should().BeFalse();
}

[Fact]
public async Task AcquireLockAsync_Works()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,6 @@ namespace CommunityToolkit.Datasync.Client.Test.Threading;
[ExcludeFromCodeCoverage]
public class DisposableLock_Tests
{
[Fact]
public void AcquireLock_Works()
{
DisposableLock sut = new();

sut.IsLocked().Should().BeFalse();
using (sut.AcquireLock())
{
sut.IsLocked().Should().BeTrue();
}

sut.IsLocked().Should().BeFalse();
}

[Fact]
public async Task AcquireLockAsync_Works()
{
Expand Down
Loading