1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

feat: implement new function

This commit is contained in:
Andreas Coroiu
2025-02-20 13:44:38 +01:00
parent aa2b0a9fe8
commit 80bb60682a
2 changed files with 179 additions and 0 deletions

View File

@@ -25,6 +25,150 @@ describe("Rc", () => {
rc = new Rc(value);
});
describe("use", () => {
describe("given Rc has not been marked for disposal", () => {
describe("given callback is synchronous", () => {
it("calls the callback", () => {
const spy = jest.fn();
rc.use(() => {
spy();
});
expect(spy).toHaveBeenCalled();
});
it("provides value in callback", () => {
rc.use((v) => {
expect(v).toBe(value);
});
});
it("increases refCount while value is in use", () => {
rc.use(() => {
expect(rc["refCount"]).toBe(1);
});
expect(rc["refCount"]).toBe(0);
});
it("does not free value when refCount reaches 0 when not marked for disposal", () => {
rc.use(() => {});
expect(value.isFreed).toBe(false);
});
it("frees value directly when marked for disposal if refCount is 0", () => {
rc.use(() => {});
rc.markForDisposal();
expect(value.isFreed).toBe(true);
});
it("frees value after refCount reaches 0 when rc is marked for disposal while in use", () => {
rc.use(() => {
rc.markForDisposal();
expect(value.isFreed).toBe(false);
});
expect(value.isFreed).toBe(true);
});
it("throws error when trying to take a disposed reference", () => {
rc.markForDisposal();
expect(() => rc.use(() => {})).toThrow();
});
});
describe("given callback is asynchronous", () => {
it("awaits the callback", async () => {
const spy = jest.fn();
await rc.use(async () => {
await new Promise((resolve) => setTimeout(resolve, 5));
spy();
});
expect(spy).toHaveBeenCalled();
});
it("provides value in callback", async () => {
await rc.use(async (v) => {
expect(v).toBe(value);
});
});
it("increases refCount while value is in use", async () => {
let resolveCallback: () => void;
const promise = new Promise<void>((resolve) => {
resolveCallback = resolve;
});
const usePromise = rc.use(async () => {
await new Promise((resolve) => setTimeout(resolve, 5));
await promise;
});
// should be 1 because the callback has not resolved yet
expect(rc["refCount"]).toBe(1);
resolveCallback();
await usePromise;
// should be 0 because the callback has resolved
expect(rc["refCount"]).toBe(0);
});
it("does not free value when refCount reaches 0 when not marked for disposal", async () => {
await rc.use(async () => {
await new Promise((resolve) => setTimeout(resolve, 5));
});
expect(value.isFreed).toBe(false);
});
it("frees value directly when marked for disposal if refCount is 0", async () => {
await rc.use(async () => {});
rc.markForDisposal();
expect(value.isFreed).toBe(true);
});
it("frees value after refCount reaches 0 when rc is marked for disposal while in use", async () => {
let resolveCallback: () => void;
const promise = new Promise<void>((resolve) => {
resolveCallback = resolve;
});
const usePromise = rc.use(async () => {
await new Promise((resolve) => setTimeout(resolve, 5));
await promise;
});
rc.markForDisposal();
// should not be freed yet because the callback has not resolved
expect(value.isFreed).toBe(false);
resolveCallback();
await usePromise;
// should be freed because the callback has resolved
expect(value.isFreed).toBe(true);
});
it("throws error when trying to take a disposed reference", async () => {
rc.markForDisposal();
await expect(async () => await rc.use(async () => {})).rejects.toThrow();
});
});
});
});
it("should increase refCount when taken", () => {
rc.take();

View File

@@ -2,6 +2,8 @@ import { UsingRequired } from "../using-required";
export type Freeable = { free: () => void };
type UseReturnValue<T> = T extends (value: any) => Promise<unknown> ? Promise<void> : void;
/**
* Reference counted disposable value.
* This class is used to manage the lifetime of a value that needs to be
@@ -16,6 +18,39 @@ export class Rc<T extends Freeable> {
this.value = value;
}
/**
* Use the value in a callback.
* The callback will be called immediately if the value is not marked for disposal.
* If the callback returns a promise, the promise will be awaited.
*
* @example
* ```typescript
* function someFunction(rc: Rc<SomeValue>) {
* using reference = rc.use((value) => {
* value.doSomething();
* });
* }
* ```
*
* @param fn The callback to call with the value.
*/
use<Callback extends (value: T) => unknown>(fn: Callback): UseReturnValue<Callback> {
if (this.markedForDisposal) {
throw new Error("Cannot use a value marked for disposal");
}
this.refCount++;
const maybePromise = fn(this.value);
if (maybePromise instanceof Promise) {
return maybePromise.then(() => {
this.release();
}) as UseReturnValue<Callback>;
} else {
this.release();
}
}
/**
* Use this function when you want to use the underlying object.
* This will guarantee that you have a reference to the object