Skip to content

Commit

Permalink
Merge pull request #247 from mittwald/export-store
Browse files Browse the repository at this point in the history
feat: provide generic store as package export
  • Loading branch information
mfal authored Sep 18, 2024
2 parents 30521dd + b06d75c commit 6e7838d
Show file tree
Hide file tree
Showing 13 changed files with 99 additions and 78 deletions.
Binary file added .yarn/cache/fsevents-patch-6b67494872-10.zip
Binary file not shown.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
"types": "./dist/types/http/index.d.ts",
"import": "./dist/esm/http/index.js"
},
"./store": {
"types": "./dist/types/store/index.d.ts",
"import": "./dist/esm/store/index.js"
},
"./package.json": "./package.json"
},
"types": "./dist/types/index.d.ts",
Expand Down
22 changes: 13 additions & 9 deletions src/http/getHttpResource.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
import { getHttpResource } from "./getHttpResource.js";
import { Store } from "../store/Store.js";
import { beforeEach } from "@jest/globals";
import { asyncResourceStore } from "../resource/store.js";

beforeEach(() => {
Store.default.clear();
asyncResourceStore.clear();
});

test("HTTP resource is tagged with method and uri", () => {
getHttpResource("https://test.org");
expect(Store.default.getAll("http/method/GET")).toHaveLength(1);
expect(Store.default.getAll("http/uri/https://test.org")).toHaveLength(1);
expect(asyncResourceStore.getAll("http/method/GET")).toHaveLength(1);
expect(asyncResourceStore.getAll("http/uri/https://test.org")).toHaveLength(
1,
);
});

test("HTTP resource is tagged with method and uri (using query params)", () => {
getHttpResource("https://test.org", { params: { search: "foo" } });
expect(Store.default.getAll("http/method/GET")).toHaveLength(1);
expect(asyncResourceStore.getAll("http/method/GET")).toHaveLength(1);
expect(
Store.default.getAll("http/uri/https://test.org?search=foo"),
asyncResourceStore.getAll("http/uri/https://test.org?search=foo"),
).toHaveLength(1);
});

test("HTTP resource is tagged with no-default method and uri", () => {
getHttpResource("https://test.org/foo", {
method: "OPTIONS",
});
expect(Store.default.getAll("http/method/OPTIONS")).toHaveLength(1);
expect(Store.default.getAll("http/uri/https://test.org/*")).toHaveLength(1);
expect(Store.default.getAll("http/uri/**/foo")).toHaveLength(1);
expect(asyncResourceStore.getAll("http/method/OPTIONS")).toHaveLength(1);
expect(asyncResourceStore.getAll("http/uri/https://test.org/*")).toHaveLength(
1,
);
expect(asyncResourceStore.getAll("http/uri/**/foo")).toHaveLength(1);
});

test("HTTP resource is the same on same request config and url", () => {
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./refresh.js";
export * from "./resource/refresh.js";
export * from "./use-promise/usePromise.js";
export * from "./resource/getAsyncResource.js";
export * from "./resource/AsyncResource.js";
export * from "./resource/resourceify.js";
export { asyncResourceStore } from "./resource/store.js";
4 changes: 2 additions & 2 deletions src/resource/getAsyncResource.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { beforeEach, expect, jest, test } from "@jest/globals";
import { sleep } from "../lib/testing.js";
import { getAsyncResource } from "./getAsyncResource.js";
import { Store } from "../store/Store.js";
import { AsyncResource } from "./AsyncResource.js";
import { asyncResourceStore } from "./store.js";

const sleepTime = 2000;

beforeEach(() => {
jest.useFakeTimers();
Store.default.clear();
asyncResourceStore.clear();
});

afterEach(() => {
Expand Down
4 changes: 2 additions & 2 deletions src/resource/getAsyncResource.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AsyncFn, FnParameters, GetAsyncResourceOptions } from "./types.js";
import { defaultStorageKeyBuilder } from "../store/defaultStorageKeyBuilder.js";
import { Store } from "../store/Store.js";
import { AsyncResource } from "./AsyncResource.js";
import { asyncResourceStore } from "./store.js";

const emptyResource = new AsyncResource<undefined>(() =>
Promise.resolve(undefined),
Expand Down Expand Up @@ -41,7 +41,7 @@ export function getAsyncResource<TValue, TParams extends FnParameters>(

const resourceBuilder = () => new AsyncResource(asyncResourceLoader);

return Store.default.getOrSet(storageKey, resourceBuilder, {
return asyncResourceStore.getOrSet(storageKey, resourceBuilder, {
tags: tags,
});
}
9 changes: 5 additions & 4 deletions src/refresh.ts → src/resource/refresh.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Store } from "./store/Store.js";
import { Tag, TagPattern } from "./store/types.js";
import { AsyncResource } from "./resource/AsyncResource.js";
import { Tag, TagPattern } from "../store/types.js";
import { AsyncResource } from "./AsyncResource.js";

import { asyncResourceStore } from "./store.js";

interface ClearOptions {
tag?: Tag | TagPattern;
Expand All @@ -13,7 +14,7 @@ export function refresh(options: ClearOptions = {}): void {
const resourceIsMatchingError = (resource: AsyncResource): boolean =>
error === undefined || resource.isMatchingError(error);

Store.default
asyncResourceStore
.getAll(tag)
.filter(resourceIsMatchingError)
.forEach((resource) => resource.refresh());
Expand Down
4 changes: 4 additions & 0 deletions src/resource/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Store } from "../store/Store.js";
import { AsyncResource } from "./AsyncResource.js";

export const asyncResourceStore = new Store<AsyncResource>();
70 changes: 37 additions & 33 deletions src/store/Store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { AsyncResource } from "../resource/AsyncResource.js";
import { beforeEach } from "@jest/globals";
import { setValue } from "../lib/EventualValue.js";

const testStore = new Store<AsyncResource>();

beforeEach(() => {
Store.default.clear();
testStore.clear();
});

const testResource1 = new AsyncResource(() => Promise.resolve("foo"));
Expand All @@ -20,105 +22,107 @@ errorTestResource2.error.updateValue(setValue(error2));

describe("get()", () => {
test("returns undefined if entry is not set", () => {
expect(Store.default.get("42")).toBeUndefined();
expect(testStore.get("42")).toBeUndefined();
});

test("returns entry if entry is set", () => {
Store.default.getOrSet("42", () => testResource1);
expect(Store.default.get("42")).toBe(testResource1);
testStore.getOrSet("42", () => testResource1);
expect(testStore.get("42")).toBe(testResource1);
});
});

describe("findByError()", () => {
test("returns all with error when set to 'true'", () => {
Store.default.getOrSet("42", () => errorTestResource1);
Store.default.getOrSet("43", () => errorTestResource2);
const all = Store.default.findByError(true);
testStore.getOrSet("42", () => errorTestResource1);
testStore.getOrSet("43", () => errorTestResource2);
const all = testStore.findBy((res) => res.isMatchingError(true));
expect(all).toHaveLength(2);
expect(all).toEqual(
expect.arrayContaining([errorTestResource1, errorTestResource2]),
);
});

test("returns only matching ones when set to an error instance", () => {
Store.default.getOrSet("42", () => errorTestResource1);
Store.default.getOrSet("43", () => errorTestResource2);
const matching = Store.default.findByError(error1);
testStore.getOrSet("42", () => errorTestResource1);
testStore.getOrSet("43", () => errorTestResource2);
const matching = testStore.findBy((res) => res.isMatchingError(error1));
expect(matching).toHaveLength(1);
expect(matching).toEqual(expect.arrayContaining([errorTestResource1]));
});

test("returns nothing when set to an error instance that is not stored", () => {
Store.default.getOrSet("42", () => errorTestResource1);
Store.default.getOrSet("43", () => errorTestResource2);
const matching = Store.default.findByError(new Error("Not found"));
testStore.getOrSet("42", () => errorTestResource1);
testStore.getOrSet("43", () => errorTestResource2);
const matching = testStore.findBy((res) =>
res.isMatchingError(new Error("Not found")),
);
expect(matching).toHaveLength(0);
});
});

describe("getOrSet()", () => {
test("inserts new entry", () => {
expect(Store.default.get("42")).toBeUndefined();
Store.default.getOrSet("42", () => testResource1);
expect(Store.default.get("42")).toBe(testResource1);
expect(testStore.get("42")).toBeUndefined();
testStore.getOrSet("42", () => testResource1);
expect(testStore.get("42")).toBe(testResource1);
});

test("does not override existing entry", () => {
Store.default.getOrSet("42", () => testResource1);
Store.default.getOrSet("42", () => testResource2);
expect(Store.default.get("42")).toBe(testResource1);
testStore.getOrSet("42", () => testResource1);
testStore.getOrSet("42", () => testResource2);
expect(testStore.get("42")).toBe(testResource1);
});
});

describe("getAll()", () => {
test("returns all entries", () => {
Store.default.getOrSet("42", () => testResource1);
Store.default.getOrSet("43", () => testResource2);
const all = Store.default.getAll();
testStore.getOrSet("42", () => testResource1);
testStore.getOrSet("43", () => testResource2);
const all = testStore.getAll();
expect(all).toHaveLength(2);
expect(all).toEqual(expect.arrayContaining([testResource1, testResource2]));
});

test("returns no entries when no tag matches", () => {
Store.default.getOrSet("42", () => testResource1, {
testStore.getOrSet("42", () => testResource1, {
tags: ["entry/42/tag/1", "entry/42/tag/2"],
});
const all = Store.default.getAll("foo");
const all = testStore.getAll("foo");
expect(all).toHaveLength(0);
});

test("returns no entries when no tag matches glob", () => {
Store.default.getOrSet("42", () => testResource1, {
testStore.getOrSet("42", () => testResource1, {
tags: ["entry/42/tag/1", "entry/42/tag/2"],
});
const all = Store.default.getAll("foo/**");
const all = testStore.getAll("foo/**");
expect(all).toHaveLength(0);
});

test("returns entry when one tag matches exactly", () => {
Store.default.getOrSet("42", () => testResource1, {
testStore.getOrSet("42", () => testResource1, {
tags: ["entry/42/tag/1", "entry/42/tag/2"],
});
const all = Store.default.getAll("entry/42/tag/1");
const all = testStore.getAll("entry/42/tag/1");
expect(all).toHaveLength(1);
});

test("returns entry when one tag matches glob", () => {
Store.default.getOrSet("42", () => testResource1, {
testStore.getOrSet("42", () => testResource1, {
tags: ["entry/42/tag/1", "entry/42/tag/2"],
});
const all = Store.default.getAll("entry/42/**");
const all = testStore.getAll("entry/42/**");
expect(all).toHaveLength(1);
});

test("returns all entries with tag matching glob", () => {
Store.default.getOrSet("42", () => testResource1, {
testStore.getOrSet("42", () => testResource1, {
tags: ["entry/42/tag/1", "entry/43/tag/2"],
});
Store.default.getOrSet("43", () => testResource2, {
testStore.getOrSet("43", () => testResource2, {
tags: ["entry/43/tag/1", "entry/43/tag/2"],
});
const all = Store.default.getAll("entry/**");
const all = testStore.getAll("entry/**");
expect(all).toHaveLength(2);
});
});
46 changes: 25 additions & 21 deletions src/store/Store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { AsyncResource } from "../resource/AsyncResource.js";
import { Minimatch } from "minimatch";
import {
TagPattern,
Expand All @@ -8,50 +7,55 @@ import {
Tag,
} from "./types.js";

export class Store {
public static default: Store = new Store();
private readonly entries = new Map<string, StorageEntry>();
export class Store<T> {
private readonly entries = new Map<string, StorageEntry<T>>();

private constructor() {
// singleton
}
public constructor() {}

public getOrSet<T extends AsyncResource>(
public getOrSet<TExtends extends T>(
id: string,
resourceBuilder: () => T,
dataBuilder: () => TExtends,
options: StorageEntryOptions = {},
): T {
): TExtends {
const { tags = [] } = options;

const existing = this.entries.get(id);

if (existing) {
return existing.resource as unknown as T;
return existing.data as unknown as TExtends;
}

const newResource = resourceBuilder();
const newData = dataBuilder();

this.entries.set(id, {
resource: newResource,
data: newData,
tags,
});

return newResource;
return newData;
}

public set<TExtends extends T>(
id: string,
dataBuilder: () => TExtends,
options: StorageEntryOptions = {},
): void {
this.getOrSet(id, dataBuilder, options);
}

public get(id: string): AsyncResource | undefined {
return this.entries.get(id)?.resource;
public get(id: string): T | undefined {
return this.entries.get(id)?.data;
}

public findByError(error: true | unknown): AsyncResource[] {
return this.getAll().filter((resource) => resource.isMatchingError(error));
public findBy(matcher: (entry: T) => boolean): T[] {
return this.getAll().filter(matcher);
}

public getAll(tag?: Tag | TagPattern): AsyncResource[] {
public getAll(tag?: Tag | TagPattern): T[] {
const entriesArray = Array.from(this.entries.values());

if (tag === undefined) {
return entriesArray.map((e) => e.resource);
return entriesArray.map((e) => e.data);
}

const mm = new Minimatch(tag);
Expand All @@ -61,7 +65,7 @@ export class Store {

return entriesArray
.filter((e) => testSomeTagsMatchingPattern(e.tags))
.map((e) => e.resource);
.map((e) => e.data);
}

public clear(): void {
Expand Down
1 change: 1 addition & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Store.js";
6 changes: 2 additions & 4 deletions src/store/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { AsyncResource } from "../resource/AsyncResource.js";

export type Tag = string;
export type TagPattern = string;
export type Tags = Tag[];
Expand All @@ -8,8 +6,8 @@ export interface StorageEntryOptions {
tags?: Tags;
}

export interface StorageEntry {
readonly resource: AsyncResource;
export interface StorageEntry<T> {
readonly data: T;
readonly tags: Tags;
}

Expand Down
Loading

0 comments on commit 6e7838d

Please sign in to comment.