@lightbend/akkaserverless-javascript-sdk
Version:
Akka Serverless JavaScript SDK
347 lines (318 loc) • 11.6 kB
JavaScript
/*
* Copyright 2021 Lightbend Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const should = require('chai').should();
const protobuf = require('protobufjs');
const path = require('path');
const replicatedData = require('../../src/replicated-data');
const ReplicatedMap = replicatedData.ReplicatedMap;
const protobufHelper = require('../../src/protobuf-helper');
const AnySupport = require('../../src/protobuf-any');
const ReplicatedEntityDelta =
protobufHelper.moduleRoot.akkaserverless.component.replicatedentity
.ReplicatedEntityDelta;
const root = new protobuf.Root();
root.loadSync(path.join(__dirname, '..', 'example.proto'));
root.resolveAll();
const Example = root.lookupType('com.example.Example');
const anySupport = new AnySupport(root);
function roundTripDelta(delta) {
return ReplicatedEntityDelta.decode(
ReplicatedEntityDelta.encode(delta).finish(),
);
}
function toAny(value) {
return AnySupport.serialize(value, true, true);
}
function fromAnys(values) {
return values.map((any) => anySupport.deserialize(any));
}
function fromEntries(entries) {
return entries.map((entry) => {
return {
key: anySupport.deserialize(entry.key),
delta: entry.delta,
};
});
}
function toMapCounterEntry(key, value) {
return { key: toAny(key), delta: { counter: { change: value } } };
}
describe('ReplicatedMap', () => {
it('should have no elements when instantiated', () => {
const map = new ReplicatedMap();
map.size.should.equal(0);
should.equal(map.getAndResetDelta(), null);
});
it('should reflect an initial delta', () => {
const map = new ReplicatedMap();
map.applyDelta(
roundTripDelta({
replicatedMap: {
added: [toMapCounterEntry('one', 5), toMapCounterEntry('two', 7)],
},
}),
anySupport,
replicatedData.createForDelta,
);
map.size.should.equal(2);
new Set(map.keys()).should.include('one', 'two');
map.asObject.one.value.should.equal(5);
map.asObject.two.value.should.equal(7);
should.equal(map.getAndResetDelta(), null);
});
it('should generate an add delta', () => {
const map = new ReplicatedMap().set(
'one',
new replicatedData.ReplicatedCounter(),
);
map.has('one').should.be.true;
map.size.should.equal(1);
const delta1 = roundTripDelta(map.getAndResetDelta());
delta1.replicatedMap.added.should.have.lengthOf(1);
const entry = fromEntries(delta1.replicatedMap.added)[0];
entry.key.should.equal('one');
entry.delta.counter.change.toNumber().should.equal(0);
should.equal(map.getAndResetDelta(), null);
map.asObject.two = new replicatedData.ReplicatedCounter();
map.asObject.two.increment(10);
map.size.should.equal(2);
const delta2 = roundTripDelta(map.getAndResetDelta());
delta2.replicatedMap.added.should.have.lengthOf(1);
const entry2 = fromEntries(delta2.replicatedMap.added)[0];
entry2.key.should.equal('two');
entry2.delta.counter.change.toNumber().should.equal(10);
should.equal(map.getAndResetDelta(), null);
delta2.replicatedMap.updated.should.have.lengthOf(0);
});
it('should generate a remove delta', () => {
const map = new ReplicatedMap()
.set('one', new replicatedData.ReplicatedCounter())
.set('two', new replicatedData.ReplicatedCounter())
.set('three', new replicatedData.ReplicatedCounter());
map.getAndResetDelta();
map.has('one').should.be.true;
map.has('two').should.be.true;
map.has('three').should.be.true;
map.size.should.equal(3);
map.delete('one').delete('two');
map.size.should.equal(1);
map.has('three').should.be.true;
map.has('one').should.be.false;
map.has('two').should.be.false;
const delta = roundTripDelta(map.getAndResetDelta());
delta.replicatedMap.removed.should.have.lengthOf(2);
fromAnys(delta.replicatedMap.removed).should.include.members([
'one',
'two',
]);
should.equal(map.getAndResetDelta(), null);
});
it('should generate an update delta', () => {
const map = new ReplicatedMap().set(
'one',
new replicatedData.ReplicatedCounter(),
);
map.getAndResetDelta();
map.get('one').increment(5);
const delta = roundTripDelta(map.getAndResetDelta());
delta.replicatedMap.updated.should.have.lengthOf(1);
const entry = fromEntries(delta.replicatedMap.updated)[0];
entry.key.should.equal('one');
entry.delta.counter.change.toNumber().should.equal(5);
should.equal(map.getAndResetDelta(), null);
});
it('should generate a clear delta', () => {
const map = new ReplicatedMap()
.set('one', new replicatedData.ReplicatedCounter())
.set('two', new replicatedData.ReplicatedCounter());
map.getAndResetDelta();
map.clear().size.should.equal(0);
const delta = roundTripDelta(map.getAndResetDelta());
delta.replicatedMap.cleared.should.be.true;
should.equal(map.getAndResetDelta(), null);
});
it('should not generate a delta when an added element is removed', () => {
const map = new ReplicatedMap().set(
'one',
new replicatedData.ReplicatedCounter(),
);
map.getAndResetDelta();
map
.set('two', new replicatedData.ReplicatedCounter())
.delete('two')
.size.should.equal(1);
should.equal(map.getAndResetDelta(), null);
});
it('should generate a delta when a removed element is added', () => {
const map = new ReplicatedMap()
.set('one', new replicatedData.ReplicatedCounter())
.set('two', new replicatedData.ReplicatedCounter());
map.getAndResetDelta();
map
.delete('two')
.set('two', new replicatedData.ReplicatedCounter())
.size.should.equal(2);
const delta = roundTripDelta(map.getAndResetDelta());
delta.replicatedMap.removed.should.have.lengthOf(1);
delta.replicatedMap.added.should.have.lengthOf(1);
delta.replicatedMap.updated.should.have.lengthOf(0);
});
it('should generate a delta when an already existing element is set', () => {
const map = new ReplicatedMap().set(
'one',
new replicatedData.ReplicatedCounter(),
);
map.getAndResetDelta();
map.set('one', new replicatedData.ReplicatedCounter()).size.should.equal(1);
const delta = roundTripDelta(map.getAndResetDelta());
delta.replicatedMap.removed.should.have.lengthOf(1);
delta.replicatedMap.added.should.have.lengthOf(1);
delta.replicatedMap.updated.should.have.lengthOf(0);
});
it('should not generate a delta when a non existing element is removed', () => {
const map = new ReplicatedMap().set(
'one',
new replicatedData.ReplicatedCounter(),
);
map.getAndResetDelta();
map.delete('two').size.should.equal(1);
should.equal(map.getAndResetDelta(), null);
});
it('should generate a delta when an already existing element is set', () => {
const map = new ReplicatedMap().set(
'one',
new replicatedData.ReplicatedCounter(),
);
map.getAndResetDelta();
map.set('one', new replicatedData.ReplicatedCounter()).size.should.equal(1);
const delta = roundTripDelta(map.getAndResetDelta());
delta.replicatedMap.removed.should.have.lengthOf(1);
delta.replicatedMap.added.should.have.lengthOf(1);
delta.replicatedMap.updated.should.have.lengthOf(0);
});
it('clear all other deltas when the set is cleared', () => {
const map = new ReplicatedMap()
.set('one', new replicatedData.ReplicatedCounter())
.set('two', new replicatedData.ReplicatedCounter());
map.getAndResetDelta();
map.asObject.two.increment(10);
map
.set('one', new replicatedData.ReplicatedCounter())
.clear()
.size.should.equal(0);
const delta = roundTripDelta(map.getAndResetDelta());
delta.replicatedMap.cleared.should.be.true;
delta.replicatedMap.added.should.have.lengthOf(0);
delta.replicatedMap.removed.should.have.lengthOf(0);
delta.replicatedMap.updated.should.have.lengthOf(0);
});
it('should reflect a delta add', () => {
const map = new ReplicatedMap().set(
'one',
new replicatedData.ReplicatedCounter(),
);
map.getAndResetDelta();
map.applyDelta(
roundTripDelta({
replicatedMap: {
added: [
{
key: toAny('two'),
delta: {
counter: { change: 4 },
},
},
],
},
}),
anySupport,
replicatedData.createForDelta,
);
map.size.should.equal(2);
new Set(map.keys()).should.include('one', 'two');
map.asObject.two.value.should.equal(4);
should.equal(map.getAndResetDelta(), null);
});
it('should reflect a delta remove', () => {
const map = new ReplicatedMap()
.set('one', new replicatedData.ReplicatedCounter())
.set('two', new replicatedData.ReplicatedCounter());
map.getAndResetDelta();
map.applyDelta(
roundTripDelta({
replicatedMap: {
removed: [toAny('two')],
},
}),
anySupport,
);
map.size.should.equal(1);
new Set(map.keys()).should.include('one');
should.equal(map.getAndResetDelta(), null);
});
it('should reflect a delta clear', () => {
const map = new ReplicatedMap()
.set('one', new replicatedData.ReplicatedCounter())
.set('two', new replicatedData.ReplicatedCounter());
map.getAndResetDelta();
map.applyDelta(
roundTripDelta({
replicatedMap: {
cleared: true,
},
}),
anySupport,
);
map.size.should.equal(0);
should.equal(map.getAndResetDelta(), null);
});
it('should work with protobuf keys', () => {
const map = new ReplicatedMap()
.set(
Example.create({ field1: 'one' }),
new replicatedData.ReplicatedCounter(),
)
.set(
Example.create({ field1: 'two' }),
new replicatedData.ReplicatedCounter(),
);
map.getAndResetDelta();
map.delete(Example.create({ field1: 'one' }));
map.size.should.equal(1);
const delta = roundTripDelta(map.getAndResetDelta());
delta.replicatedMap.removed.should.have.lengthOf(1);
fromAnys(delta.replicatedMap.removed)[0].field1.should.equal('one');
});
it('should work with json types', () => {
const map = new ReplicatedMap()
.set({ foo: 'bar' }, new replicatedData.ReplicatedCounter())
.set({ foo: 'baz' }, new replicatedData.ReplicatedCounter());
map.getAndResetDelta();
map.delete({ foo: 'bar' });
map.size.should.equal(1);
const delta = roundTripDelta(map.getAndResetDelta());
delta.replicatedMap.removed.should.have.lengthOf(1);
fromAnys(delta.replicatedMap.removed)[0].foo.should.equal('bar');
});
it('should support empty initial deltas (for ReplicatedMap added)', () => {
const map = new ReplicatedMap();
map.size.should.equal(0);
should.equal(map.getAndResetDelta(), null);
roundTripDelta(
map.getAndResetDelta(/* initial = */ true),
).replicatedMap.added.should.have.lengthOf(0);
});
});