Skip to content

Commit 3854092

Browse files
committed
Merge branch 'jgilles/unsubscribe-fix' of https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk into jgilles/unsubscribe-fix
2 parents ff10925 + 2fde31b commit 3854092

File tree

4 files changed

+77
-10
lines changed

4 files changed

+77
-10
lines changed

.github/workflows/check-pr-base.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ permissions: read-all
88

99
jobs:
1010
check_base_ref:
11-
name: Only release branches may merge into master
11+
name: Release branch restriction
1212
runs-on: ubuntu-latest
1313
steps:
1414
- id: not_based_on_master
1515
if: |
1616
github.event_name == 'pull_request' &&
17-
github.event.pull_request.base.ref == 'master' &&
17+
github.event.pull_request.base.ref == 'release/latest' &&
1818
! startsWith(github.event.pull_request.head.ref, 'release/')
1919
run: |
20-
echo 'Only `release/*` branches are allowed to merge into `master`.'
21-
echo 'Maybe your PR should be merging into `staging`?'
20+
echo 'Only `release/*` branches are allowed to merge into the release branch `release/latest`.'
21+
echo 'Maybe you want to change your PR base to `master`?'
2222
exit 1

src/MultiDictionary.cs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ public static MultiDictionary<TKey, TValue> FromEnumerable(IEnumerable<KeyValueP
5656

5757
/// <summary>
5858
/// Add a key-value-pair to the multidictionary.
59-
/// If the key is already present, its associated value must satisfy value.Equals(item.Value).
59+
/// If the key is already present, its associated value must satisfy
60+
/// keyComparer.Equals(value, item.Value).
6061
/// </summary>
6162
/// <param name="item"></param>
6263
/// <returns>Whether the key is entirely new to the dictionary. If it was already present, we assert that the old value is equal to the new value.</returns>
@@ -136,7 +137,7 @@ public bool Equals(MultiDictionary<TKey, TValue> other)
136137
if (other.RawDict.TryGetValue(key, out var otherVM))
137138
{
138139
var (otherValue, otherMultiplicity) = otherVM;
139-
if (!(value != null && value.Equals(otherValue) && multiplicity == otherMultiplicity))
140+
if (!(ValueComparer.Equals(value, otherValue) && multiplicity == otherMultiplicity))
140141
{
141142
return false;
142143
}
@@ -290,11 +291,18 @@ public override string ToString()
290291
}
291292

292293
/// <summary>
293-
/// A delta between two multidictionaries.
294+
/// A bulk change to a multidictionary. Allows both adding and removing rows.
294295
///
295296
/// Can be applied to a multidictionary, and also inspected before application to see
296297
/// what rows will be deleted. (This is used for OnBeforeDelete.)
297298
///
299+
/// Curiously, the order of operations applied to a MultiDictionaryDelta does not matter.
300+
/// No matter the order of Add and Remove calls on a delta, when the Delta is applied,
301+
/// the result will be the same, as long as the Add and Remove *counts* for each KeyValuePair are
302+
/// the same.
303+
/// (This means that this is a "conflict-free replicated data type", unlike MultiDictionary.)
304+
/// (MultiDictionary would also be "conflict-free" if it didn't support Remove.)
305+
///
298306
/// The delta may include value updates.
299307
/// A value can be updated multiple times, but each update must set the result to the same value.
300308
/// When applying a delta, if the target multidictionary has multiple copies of (key, value) pair,
@@ -305,12 +313,12 @@ public override string ToString()
305313
/// </summary>
306314
/// <typeparam name="TKey"></typeparam>
307315
/// <typeparam name="TValue"></typeparam>
308-
internal struct MultiDictionaryDelta<TKey, TValue>
316+
internal struct MultiDictionaryDelta<TKey, TValue> : IEquatable<MultiDictionaryDelta<TKey, TValue>>
309317
{
310318
/// <summary>
311319
/// For each key, track its NEW value (or old value, but only if we have never seen the new value).
312320
/// Also track the number of times it has been removed and inserted.
313-
/// We keep these separate so that we can track that a KVP has been removed enough times (in case
321+
/// We keep these separate so that we can debug-assert that a KVP has been removed enough times (in case
314322
/// there are multiple copies of the KVP in the map we get applied to.)
315323
/// </summary>
316324
readonly Dictionary<TKey, (TValue Value, uint Removes, uint Inserts)> RawDict;
@@ -333,7 +341,8 @@ public MultiDictionaryDelta(IEqualityComparer<TKey> keyComparer, IEqualityCompar
333341

334342
/// <summary>
335343
/// Add a key-value-pair to the multidictionary.
336-
/// If the key is already present, its associated value must satisfy value.Equals(item.Value).
344+
/// If the key is already present, its associated value must satisfy
345+
/// keyComparer.Equals(value, item.Value).
337346
/// </summary>
338347
/// <param name="item"></param>
339348
public void Add(TKey key, TValue value)
@@ -397,6 +406,23 @@ public override string ToString()
397406
return result.ToString();
398407
}
399408

409+
public bool Equals(MultiDictionaryDelta<TKey, TValue> other)
410+
{
411+
foreach (var item in RawDict)
412+
{
413+
var (key, my) = item;
414+
if (other.RawDict.TryGetValue(key, out var their))
415+
{
416+
if (!(ValueComparer.Equals(my.Value, their.Value) && my.Inserts == their.Inserts && my.Removes == their.Removes))
417+
{
418+
return false;
419+
}
420+
}
421+
}
422+
423+
return true;
424+
}
425+
400426
public readonly IEnumerable<KeyValuePair<TKey, (TValue Value, uint Removes, uint Inserts)>> Entries
401427
{
402428
get

src/SpacetimeDBClient.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,10 @@ void PreProcessTable(IRemoteTableHandle table, TableUpdate update, ProcessedData
448448
foreach (var cqu in update.Updates)
449449
{
450450
var qu = DecompressDecodeQueryUpdate(cqu);
451+
452+
// Because we are accumulating into a MultiDictionaryDelta that will be applied all-at-once
453+
// to the table, it doesn't matter that we call Add before Remove here.
454+
451455
foreach (var bin in BsatnRowListIter(qu.Inserts))
452456
{
453457
var obj = Decode(table, bin, out var pk);

tests~/MultiDictionaryTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,43 @@ public void Removals()
104104
});
105105
}
106106

107+
// Check that MultiDictionaryDelta is in fact a CRDT.
108+
[Fact]
109+
public void ShuffleDelta()
110+
{
111+
ListWithRemovals(Gen.Byte[1, 10], Gen.Byte[1, 10], EqualityComparer<byte>.Default).Sample((list, removals) =>
112+
{
113+
var m1 = new MultiDictionaryDelta<byte, byte>(EqualityComparer<byte>.Default, EqualityComparer<byte>.Default);
114+
var m2 = new MultiDictionaryDelta<byte, byte>(EqualityComparer<byte>.Default, EqualityComparer<byte>.Default);
115+
var listRemovals = list.Zip(removals).ToList();
116+
foreach (var (kvp, remove) in listRemovals)
117+
{
118+
if (remove)
119+
{
120+
m1.Remove(kvp.Key, kvp.Value);
121+
}
122+
else
123+
{
124+
m1.Add(kvp.Key, kvp.Value);
125+
}
126+
}
127+
Gen.Shuffle(listRemovals);
128+
foreach (var (kvp, remove) in listRemovals)
129+
{
130+
if (remove)
131+
{
132+
m2.Remove(kvp.Key, kvp.Value);
133+
}
134+
else
135+
{
136+
m2.Add(kvp.Key, kvp.Value);
137+
}
138+
}
139+
140+
Assert.Equal(m1, m2);
141+
});
142+
}
143+
107144
// Note: this does not check proper batch updates yet, since I wasn't sure how to randomly generate them properly.
108145
[Fact]
109146
public void ChunkedRemovals()

0 commit comments

Comments
 (0)