LibJS: Implement the AsyncDisposableStack interface

This is very similar to the DisposableStack interface, except disposal
of resources is promise-based.
This commit is contained in:
Timothy Flynn 2025-01-17 10:09:01 -05:00 committed by Andreas Kling
commit 26c2484c2f
Notes: github-actions[bot] 2025-01-17 19:47:28 +00:00
22 changed files with 873 additions and 0 deletions

View file

@ -0,0 +1,21 @@
test("constructor properties", () => {
expect(AsyncDisposableStack).toHaveLength(0);
expect(AsyncDisposableStack.name).toBe("AsyncDisposableStack");
});
describe("errors", () => {
test("called without new", () => {
expect(() => {
AsyncDisposableStack();
}).toThrowWithMessage(
TypeError,
"AsyncDisposableStack constructor must be called with 'new'"
);
});
});
describe("normal behavior", () => {
test("typeof", () => {
expect(typeof new AsyncDisposableStack()).toBe("object");
});
});

View file

@ -0,0 +1,9 @@
test("length is 0", () => {
expect(AsyncDisposableStack.prototype[Symbol.asyncDispose]).toHaveLength(0);
});
test("is the same as disposeAsync", () => {
expect(AsyncDisposableStack.prototype[Symbol.asyncDispose]).toBe(
AsyncDisposableStack.prototype.disposeAsync
);
});

View file

@ -0,0 +1,3 @@
test("basic functionality", () => {
expect(AsyncDisposableStack.prototype[Symbol.toStringTag]).toBe("AsyncDisposableStack");
});

View file

@ -0,0 +1,95 @@
test("length is 2", () => {
expect(AsyncDisposableStack.prototype.adopt).toHaveLength(2);
});
describe("basic functionality", () => {
test("adopted dispose method gets called when stack is disposed", async () => {
const stack = new AsyncDisposableStack();
let disposedCalled = 0;
let disposeArgument = undefined;
expect(disposedCalled).toBe(0);
const result = stack.adopt(null, arg => {
disposeArgument = arg;
++disposedCalled;
});
expect(result).toBeNull();
expect(disposedCalled).toBe(0);
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
expect(disposeArgument).toBeNull();
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
});
test("can adopt any value", async () => {
const stack = new AsyncDisposableStack();
const disposed = [];
function dispose(value) {
disposed.push(value);
}
const values = [null, undefined, 1, "a", Symbol.dispose, () => {}, new WeakMap(), [], {}];
values.forEach(value => {
stack.adopt(value, dispose);
});
await stack.disposeAsync();
expect(disposed).toEqual(values.reverse());
});
test("adopted stack is already disposed", async () => {
const stack = new AsyncDisposableStack();
stack.adopt(stack, value => {
expect(stack).toBe(value);
expect(stack.disposed).toBeTrue();
});
await stack.disposeAsync();
});
});
describe("throws errors", () => {
test("if call back is not a function throws type error", () => {
const stack = new AsyncDisposableStack();
[
1,
1n,
"a",
Symbol.dispose,
NaN,
0,
{},
[],
{ f() {} },
{ [Symbol.dispose]() {} },
{
get [Symbol.dispose]() {
return () => {};
},
},
].forEach(value => {
expect(() => stack.adopt(null, value)).toThrowWithMessage(TypeError, "not a function");
});
expect(stack.disposed).toBeFalse();
});
test("adopt throws if stack is already disposed (over type errors)", async () => {
const stack = new AsyncDisposableStack();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
[{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => {
expect(() => stack.adopt(value, () => {})).toThrowWithMessage(
ReferenceError,
"AsyncDisposableStack already disposed values"
);
expect(() => stack.adopt(null, value)).toThrowWithMessage(
ReferenceError,
"AsyncDisposableStack already disposed values"
);
});
});
});

View file

@ -0,0 +1,70 @@
test("length is 1", () => {
expect(AsyncDisposableStack.prototype.defer).toHaveLength(1);
});
describe("basic functionality", () => {
test("deferred function gets called when stack is disposed", async () => {
const stack = new AsyncDisposableStack();
let disposedCalled = 0;
expect(disposedCalled).toBe(0);
const result = stack.defer((...args) => {
expect(args.length).toBe(0);
++disposedCalled;
});
expect(result).toBeUndefined();
expect(disposedCalled).toBe(0);
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
});
test("deferred stack is already disposed", async () => {
const stack = new AsyncDisposableStack();
stack.defer(() => {
expect(stack.disposed).toBeTrue();
});
await stack.disposeAsync();
});
});
describe("throws errors", () => {
test("if call back is not a function throws type error", () => {
const stack = new AsyncDisposableStack();
[
1,
1n,
"a",
Symbol.dispose,
NaN,
0,
{},
[],
{ f() {} },
{ [Symbol.dispose]() {} },
{
get [Symbol.dispose]() {
return () => {};
},
},
].forEach(value => {
expect(() => stack.defer(value)).toThrowWithMessage(TypeError, "not a function");
});
expect(stack.disposed).toBeFalse();
});
test("defer throws if stack is already disposed (over type errors)", async () => {
const stack = new AsyncDisposableStack();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
[{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => {
expect(() => stack.defer(value)).toThrowWithMessage(
ReferenceError,
"AsyncDisposableStack already disposed values"
);
});
});
});

View file

@ -0,0 +1,88 @@
test("length is 0", () => {
expect(AsyncDisposableStack.prototype.disposeAsync).toHaveLength(0);
});
describe("basic functionality", () => {
test("make the stack marked as disposed", async () => {
const stack = new AsyncDisposableStack();
const result = await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
expect(result).toBeUndefined();
});
test("call dispose on objects in stack when called", async () => {
const stack = new AsyncDisposableStack();
let disposedCalled = false;
stack.use({
[Symbol.asyncDispose]() {
disposedCalled = true;
},
});
expect(disposedCalled).toBeFalse();
const result = await stack.disposeAsync();
expect(disposedCalled).toBeTrue();
expect(result).toBeUndefined();
});
test("disposed the objects added to the stack in reverse order", async () => {
const disposed = [];
const stack = new AsyncDisposableStack();
stack.use({
[Symbol.asyncDispose]() {
disposed.push("a");
},
});
stack.use({
[Symbol.asyncDispose]() {
disposed.push("b");
},
});
expect(disposed).toEqual([]);
const result = await stack.disposeAsync();
expect(disposed).toEqual(["b", "a"]);
expect(result).toBeUndefined();
});
test("does not dispose anything if already disposed", async () => {
const disposed = [];
const stack = new AsyncDisposableStack();
stack.use({
[Symbol.asyncDispose]() {
disposed.push("a");
},
});
expect(stack.disposed).toBeFalse();
expect(disposed).toEqual([]);
let result = await stack.disposeAsync();
expect(result).toBeUndefined();
expect(stack.disposed).toBeTrue();
expect(disposed).toEqual(["a"]);
result = await stack.disposeAsync();
expect(result).toBeUndefined();
expect(stack.disposed).toBeTrue();
expect(disposed).toEqual(["a"]);
});
test("throws if dispose method throws", async () => {
const stack = new AsyncDisposableStack();
let disposedCalled = false;
stack.use({
[Symbol.asyncDispose]() {
disposedCalled = true;
expect().fail("fail in dispose");
},
});
expect(async () => await stack.disposeAsync()).toThrowWithMessage(
ExpectationError,
"fail in dispose"
);
});
});

View file

@ -0,0 +1,24 @@
test("is getter without setter", () => {
const property = Object.getOwnPropertyDescriptor(AsyncDisposableStack.prototype, "disposed");
expect(property.get).not.toBeUndefined();
expect(property.set).toBeUndefined();
expect(property.value).toBeUndefined();
});
describe("basic functionality", () => {
test("is not a property on the object itself", () => {
const stack = new AsyncDisposableStack();
expect(Object.hasOwn(stack, "disposed")).toBeFalse();
});
test("starts off as false", () => {
const stack = new AsyncDisposableStack();
expect(stack.disposed).toBeFalse();
});
test("becomes true after being disposed", async () => {
const stack = new AsyncDisposableStack();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
});
});

View file

@ -0,0 +1,62 @@
test("length is 0", () => {
expect(AsyncDisposableStack.prototype.move).toHaveLength(0);
});
describe("basic functionality", () => {
test("stack is disposed after moving", () => {
const stack = new AsyncDisposableStack();
const newStack = stack.move();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeFalse();
});
test("move does not dispose resource but only move them", async () => {
const stack = new AsyncDisposableStack();
let disposeCalled = false;
stack.defer(() => {
disposeCalled = true;
});
expect(disposeCalled).toBeFalse();
expect(stack.disposed).toBeFalse();
const newStack = stack.move();
expect(disposeCalled).toBeFalse();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeFalse();
await stack.disposeAsync();
expect(disposeCalled).toBeFalse();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeFalse();
await newStack.disposeAsync();
expect(disposeCalled).toBeTrue();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeTrue();
});
test("can add stack to itself", async () => {
const stack = new AsyncDisposableStack();
stack.move(stack);
await stack.disposeAsync();
});
});
describe("throws errors", () => {
test("move throws if stack is already disposed (over type errors)", async () => {
const stack = new AsyncDisposableStack();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
expect(() => stack.move()).toThrowWithMessage(
ReferenceError,
"AsyncDisposableStack already disposed values"
);
});
});

View file

@ -0,0 +1,96 @@
test("length is 1", () => {
expect(AsyncDisposableStack.prototype.use).toHaveLength(1);
});
describe("basic functionality", () => {
test("added objects dispose method gets when stack is disposed", async () => {
const stack = new AsyncDisposableStack();
let disposedCalled = 0;
const obj = {
[Symbol.dispose]() {
++disposedCalled;
},
};
expect(disposedCalled).toBe(0);
const result = stack.use(obj);
expect(result).toBe(obj);
expect(disposedCalled).toBe(0);
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
});
test("can add null and undefined", async () => {
const stack = new AsyncDisposableStack();
expect(stack.use(null)).toBeNull();
expect(stack.use(undefined)).toBeUndefined();
expect(stack.disposed).toBeFalse();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
});
test("can add stack to itself", async () => {
const stack = new AsyncDisposableStack();
stack.use(stack);
await stack.disposeAsync();
});
});
describe("throws errors", () => {
test("if added value is not an object or null or undefined throws type error", () => {
const stack = new AsyncDisposableStack();
[1, 1n, "a", Symbol.dispose, NaN, 0].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(TypeError, "not an object");
});
expect(stack.disposed).toBeFalse();
});
test("if added object does not have a dispose method throws type error", () => {
const stack = new AsyncDisposableStack();
[{}, [], { f() {} }].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(
TypeError,
"does not have dispose method"
);
});
expect(stack.disposed).toBeFalse();
});
test("if added object has non function dispose method it throws type error", () => {
const stack = new AsyncDisposableStack();
let calledGetter = false;
[
{ [Symbol.dispose]: 1 },
{
get [Symbol.dispose]() {
calledGetter = true;
return 1;
},
},
].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(TypeError, "is not a function");
});
expect(stack.disposed).toBeFalse();
expect(calledGetter).toBeTrue();
});
test("use throws if stack is already disposed (over type errors)", async () => {
const stack = new AsyncDisposableStack();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
[{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(
ReferenceError,
"AsyncDisposableStack already disposed values"
);
});
});
});