gristlabs_grist-core/test/common/RefCountMap.ts

171 lines
5.9 KiB
TypeScript
Raw Permalink Normal View History

import {delay} from 'app/common/delay';
import {RefCountMap} from 'app/common/RefCountMap';
import {assert} from 'chai';
import * as sinon from 'sinon';
function assertResetSingleCall(spy: sinon.SinonSpy, context: any, ...args: any[]): void {
sinon.assert.calledOnce(spy);
sinon.assert.calledOn(spy, context);
sinon.assert.calledWithExactly(spy, ...args);
spy.resetHistory();
}
describe("RefCountMap", function() {
it("should dispose items when ref-count returns to 0", function() {
const create = sinon.stub().callsFake((key) => key.toUpperCase());
const dispose = sinon.spy();
const m = new RefCountMap<string, string>({create, dispose, gracePeriodMs: 0});
const subFoo1 = m.use("foo");
assert.strictEqual(subFoo1.get(), "FOO");
assertResetSingleCall(create, null, "foo");
const subBar1 = m.use("bar");
assert.strictEqual(subBar1.get(), "BAR");
assertResetSingleCall(create, null, "bar");
const subFoo2 = m.use("foo");
assert.strictEqual(subFoo2.get(), "FOO");
sinon.assert.notCalled(create);
// Now dispose one by one.
subFoo1.dispose();
sinon.assert.notCalled(dispose);
subBar1.dispose();
assertResetSingleCall(dispose, null, "bar", "BAR");
// An extra subscription increases refCount, so subFoo2.dispose will not yet dispose it.
const subFoo3 = m.use("foo");
assert.strictEqual(subFoo3.get(), "FOO");
sinon.assert.notCalled(create);
subFoo2.dispose();
sinon.assert.notCalled(dispose);
subFoo3.dispose();
assertResetSingleCall(dispose, null, "foo", "FOO");
});
it("should respect the grace period", async function() {
const create = sinon.stub().callsFake((key) => key.toUpperCase());
const dispose = sinon.spy();
const m = new RefCountMap<string, string>({create, dispose, gracePeriodMs: 60});
const subFoo1 = m.use("foo");
assert.strictEqual(subFoo1.get(), "FOO");
assertResetSingleCall(create, null, "foo");
const subBar1 = m.use("bar");
assert.strictEqual(subBar1.get(), "BAR");
assertResetSingleCall(create, null, "bar");
// Disposal is not immediate, we have some time.
subFoo1.dispose();
subBar1.dispose();
sinon.assert.notCalled(dispose);
// Wait a bit and add more usage to one of the keys.
await delay(30);
const subFoo2 = m.use("foo");
assert.strictEqual(subFoo2.get(), "FOO");
sinon.assert.notCalled(create);
// Grace period hasn't expired yet, so dispose isn't called yet.
sinon.assert.notCalled(dispose);
// Now wait for the grace period to end.
await delay(40);
// Ensure that bar's disposal has run now, but not foo's.
assertResetSingleCall(dispose, null, "bar", "BAR");
// Dispose the second usage, and wait for the full grace period.
subFoo2.dispose();
await delay(70);
assertResetSingleCall(dispose, null, "foo", "FOO");
});
it("should dispose immediately on clear", async function() {
const create = sinon.stub().callsFake((key) => key.toUpperCase());
const dispose = sinon.spy();
const m = new RefCountMap<string, string>({create, dispose, gracePeriodMs: 0});
const subFoo1 = m.use("foo");
const subBar1 = m.use("bar");
const subFoo2 = m.use("foo");
m.dispose();
assert.equal(dispose.callCount, 2);
assert.deepEqual(dispose.args, [["foo", "FOO"], ["bar", "BAR"]]);
dispose.resetHistory();
// Should be a no-op to dispose subscriptions after RefCountMap is disposed.
subFoo1.dispose();
subFoo2.dispose();
subBar1.dispose();
sinon.assert.notCalled(dispose);
// It should not be a matter of gracePeriod, but make sure by waiting a bit.
await delay(30);
sinon.assert.notCalled(dispose);
});
it("should be safe to purge a key", async function() {
const create = sinon.stub().callsFake((key) => key.toUpperCase());
const dispose = sinon.spy();
const m = new RefCountMap<string, string>({create, dispose, gracePeriodMs: 0});
const subFoo1 = m.use("foo");
const subBar1 = m.use("bar");
const subFoo2 = m.use("foo");
m.purgeKey("foo");
assertResetSingleCall(dispose, null, "foo", "FOO");
m.purgeKey("bar");
assertResetSingleCall(dispose, null, "bar", "BAR");
// The tricky case is when a new "foo" key is created after the purge.
const subFooNew1 = m.use("foo");
const subBarNew1 = m.use("bar");
// Should be a no-op to dispose purged subscriptions.
subFoo1.dispose();
subFoo2.dispose();
sinon.assert.notCalled(dispose);
// A new subscription with the same key should get disposed though.
subFooNew1.dispose();
assertResetSingleCall(dispose, null, "foo", "FOO");
subBarNew1.dispose();
assertResetSingleCall(dispose, null, "bar", "BAR");
// Still a no-op to dispose old purged subscriptions.
subBar1.dispose();
sinon.assert.notCalled(dispose);
// Ensure there are no scheduled disposals due to some other bug.
await delay(30);
sinon.assert.notCalled(dispose);
});
it("should not dispose a re-created key on timeout after purge", async function() {
const create = sinon.stub().callsFake((key) => key.toUpperCase());
const dispose = sinon.spy();
const m = new RefCountMap<string, string>({create, dispose, gracePeriodMs: 60});
const subFoo1 = m.use("foo");
subFoo1.dispose(); // This schedules a disposal in 20ms
m.purgeKey("foo"); // This should purge immediately AND unset the scheduled disposal
assertResetSingleCall(dispose, null, "foo", "FOO");
await delay(20);
const subFoo2 = m.use("foo"); // Should not be affected by the scheduled disposal.
await delay(100); // "foo" stays beyond grace period, since it's being used.
sinon.assert.notCalled(dispose);
subFoo2.dispose(); // Once disposed, it stays for grace period
await delay(20);
sinon.assert.notCalled(dispose);
await delay(100); // And gets disposed after it.
assertResetSingleCall(dispose, null, "foo", "FOO");
});
});