@raubjo/architect-core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -0
- package/bun.lock +82 -1
- package/package.json +55 -6
- package/src/cache/cache.ts +2 -2
- package/src/cache/manager.ts +93 -83
- package/src/config/adapters/esm.ts +26 -0
- package/src/config/clone.ts +5 -5
- package/src/config/env.global.d.ts +2 -1
- package/src/config/env.ts +53 -55
- package/src/config/env_test.helpers.ts +58 -0
- package/src/config/repository.ts +180 -142
- package/src/container/adapters/builtin.ts +347 -0
- package/src/container/adapters/inversify.ts +123 -0
- package/src/container/contract.ts +58 -0
- package/src/container/runtime.ts +149 -0
- package/src/filesystem/adapters/local.ts +92 -83
- package/src/filesystem/adapters/local_test.helpers.ts +50 -0
- package/src/filesystem/filesystem.ts +11 -11
- package/src/foundation/application.ts +205 -175
- package/src/foundation/application_test.helpers.ts +31 -0
- package/src/index.ts +15 -6
- package/src/react.ts +2 -0
- package/src/renderers/adapters/react.tsx +35 -0
- package/src/renderers/adapters/solid.tsx +32 -0
- package/src/renderers/adapters/svelte.ts +70 -0
- package/src/renderers/adapters/vue.ts +28 -0
- package/src/renderers/contract.ts +15 -0
- package/src/runtimes/react.tsx +24 -12
- package/src/runtimes/solid.tsx +30 -0
- package/src/runtimes/svelte.ts +23 -0
- package/src/runtimes/vue.ts +20 -0
- package/src/solid.ts +2 -0
- package/src/storage/adapters/contract.ts +10 -0
- package/src/storage/adapters/indexed-db.ts +170 -156
- package/src/storage/adapters/local-storage.ts +34 -34
- package/src/storage/adapters/memory.ts +25 -25
- package/src/storage/manager.ts +65 -61
- package/src/storage/storage.ts +1 -8
- package/src/support/facades/cache.ts +40 -40
- package/src/support/facades/config.ts +78 -48
- package/src/support/facades/facade.ts +43 -28
- package/src/support/facades/storage.ts +41 -41
- package/src/support/service-provider.ts +11 -11
- package/src/support/str.ts +94 -90
- package/src/support/str_test.helpers.ts +26 -0
- package/src/svelte.ts +2 -0
- package/src/vue.ts +2 -0
- package/tsconfig.json +16 -0
- package/coverage/lcov.info +0 -1078
- package/src/config/app.ts +0 -5
- package/src/rendering/adapters/react.tsx +0 -27
- package/src/rendering/renderer.ts +0 -13
- package/src/support/providers/config-service-provider.ts +0 -19
- package/tests/application.test.ts +0 -236
- package/tests/cache-facade.test.ts +0 -45
- package/tests/cache.test.ts +0 -68
- package/tests/config-clone.test.ts +0 -31
- package/tests/config-env.test.ts +0 -88
- package/tests/config-facade.test.ts +0 -96
- package/tests/config-repository.test.ts +0 -124
- package/tests/facade-base.test.ts +0 -80
- package/tests/filesystem.test.ts +0 -81
- package/tests/runtime-react.test.tsx +0 -37
- package/tests/service-provider.test.ts +0 -23
- package/tests/storage-facade.test.ts +0 -46
- package/tests/storage.test.ts +0 -264
- package/tests/str.test.ts +0 -73
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import ConfigRepository from "../src/config/repository";
|
|
3
|
-
|
|
4
|
-
describe("ConfigRepository", () => {
|
|
5
|
-
test("gets nested values by dot key", () => {
|
|
6
|
-
const repository = new ConfigRepository({
|
|
7
|
-
app: {
|
|
8
|
-
name: "IOC Application",
|
|
9
|
-
timezone: "UTC",
|
|
10
|
-
},
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
expect(repository.get("app.name")).toBe("IOC Application");
|
|
14
|
-
expect(repository.get("app.timezone")).toBe("UTC");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("supports defaults and lazy default callbacks", () => {
|
|
18
|
-
const repository = new ConfigRepository({});
|
|
19
|
-
|
|
20
|
-
expect(repository.get("app.locale", "en")).toBe("en");
|
|
21
|
-
expect(repository.get("app.name", () => "fallback")).toBe("fallback");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("supports set, prepend, and push", () => {
|
|
25
|
-
const repository = new ConfigRepository({
|
|
26
|
-
app: {
|
|
27
|
-
middlewares: ["auth"],
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
repository.prepend("app.middlewares", "throttle");
|
|
32
|
-
repository.push("app.middlewares", "verified");
|
|
33
|
-
repository.set("app.name", "IOC");
|
|
34
|
-
|
|
35
|
-
expect(repository.get("app.name")).toBe("IOC");
|
|
36
|
-
expect(repository.array("app.middlewares")).toEqual([
|
|
37
|
-
"throttle",
|
|
38
|
-
"auth",
|
|
39
|
-
"verified",
|
|
40
|
-
]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("supports getMany with list and defaults map", () => {
|
|
44
|
-
const repository = new ConfigRepository({
|
|
45
|
-
app: { name: "IOC Application" },
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
expect(repository.getMany(["app.name", "app.locale"])).toEqual({
|
|
49
|
-
"app.name": "IOC Application",
|
|
50
|
-
"app.locale": null,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
expect(
|
|
54
|
-
repository.getMany({
|
|
55
|
-
"app.name": "fallback",
|
|
56
|
-
"app.locale": "en",
|
|
57
|
-
}),
|
|
58
|
-
).toEqual({
|
|
59
|
-
"app.name": "IOC Application",
|
|
60
|
-
"app.locale": "en",
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("typed accessors return expected values", () => {
|
|
65
|
-
const repository = new ConfigRepository({
|
|
66
|
-
app: {
|
|
67
|
-
name: "IOC",
|
|
68
|
-
retries: 3,
|
|
69
|
-
ratio: 0.75,
|
|
70
|
-
enabled: true,
|
|
71
|
-
tags: ["alpha"],
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
expect(repository.string("app.name")).toBe("IOC");
|
|
76
|
-
expect(repository.integer("app.retries")).toBe(3);
|
|
77
|
-
expect(repository.float("app.ratio")).toBe(0.75);
|
|
78
|
-
expect(repository.boolean("app.enabled")).toBe(true);
|
|
79
|
-
expect(repository.array("app.tags")).toEqual(["alpha"]);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("typed accessors throw when type does not match", () => {
|
|
83
|
-
const repository = new ConfigRepository({
|
|
84
|
-
app: {
|
|
85
|
-
name: 123,
|
|
86
|
-
retries: "3",
|
|
87
|
-
ratio: "0.75",
|
|
88
|
-
enabled: "yes",
|
|
89
|
-
tags: "nope",
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
expect(() => repository.string("app.name")).toThrow(
|
|
94
|
-
"Configuration value [app.name] is not a string.",
|
|
95
|
-
);
|
|
96
|
-
expect(() => repository.integer("app.retries")).toThrow(
|
|
97
|
-
"Configuration value [app.retries] is not an integer.",
|
|
98
|
-
);
|
|
99
|
-
expect(() => repository.float("app.ratio")).toThrow(
|
|
100
|
-
"Configuration value [app.ratio] is not a float.",
|
|
101
|
-
);
|
|
102
|
-
expect(() => repository.boolean("app.enabled")).toThrow(
|
|
103
|
-
"Configuration value [app.enabled] is not a boolean.",
|
|
104
|
-
);
|
|
105
|
-
expect(() => repository.array("app.tags")).toThrow(
|
|
106
|
-
"Configuration value [app.tags] is not an array.",
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("supports offset-style helpers", () => {
|
|
111
|
-
const repository = new ConfigRepository({
|
|
112
|
-
app: { name: "IOC Application" },
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
expect(repository.offsetExists("app.name")).toBe(true);
|
|
116
|
-
expect(repository.offsetGet("app.name")).toBe("IOC Application");
|
|
117
|
-
|
|
118
|
-
repository.offsetSet("app.name", "Changed");
|
|
119
|
-
expect(repository.offsetGet("app.name")).toBe("Changed");
|
|
120
|
-
|
|
121
|
-
repository.offsetUnset("app.name");
|
|
122
|
-
expect(repository.offsetExists("app.name")).toBe(false);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { Container } from "inversify";
|
|
3
|
-
import { Application } from "../src/foundation/application";
|
|
4
|
-
import Facade from "../src/support/facades/facade";
|
|
5
|
-
|
|
6
|
-
class BrokenFacade extends Facade {
|
|
7
|
-
static callAccessorForTest() {
|
|
8
|
-
return (this as unknown as { getFacadeAccessor: () => unknown }).getFacadeAccessor();
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
class MethodFacade extends Facade {
|
|
13
|
-
protected static getFacadeAccessor() {
|
|
14
|
-
return "method.target";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
static callMethod<T>(method: string, ...args: unknown[]) {
|
|
18
|
-
return (
|
|
19
|
-
this as unknown as {
|
|
20
|
-
callFacadeMethod: <R>(methodName: string, ...values: unknown[]) => R;
|
|
21
|
-
}
|
|
22
|
-
).callFacadeMethod<T>(method, ...args);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
class ConcreteFacade extends Facade {
|
|
27
|
-
protected static getFacadeAccessor() {
|
|
28
|
-
return "noop";
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("Facade base", () => {
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
Facade.clearResolvedInstances();
|
|
35
|
-
(Application as unknown as { container: Container | null }).container = null;
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("throws when accessor is not implemented", () => {
|
|
39
|
-
expect(() => BrokenFacade.callAccessorForTest()).toThrow(
|
|
40
|
-
"Facade does not implement getFacadeAccessor().",
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("callFacadeMethod dispatches to target method", () => {
|
|
45
|
-
const container = new Container();
|
|
46
|
-
container.bind("method.target").toConstantValue({
|
|
47
|
-
sum: (a: number, b: number) => a + b,
|
|
48
|
-
});
|
|
49
|
-
(Application as unknown as { container: Container | null }).container = container;
|
|
50
|
-
|
|
51
|
-
expect(MethodFacade.callMethod<number>("sum", 2, 3)).toBe(5);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("callFacadeMethod throws for missing target method", () => {
|
|
55
|
-
const container = new Container();
|
|
56
|
-
container.bind("method.target").toConstantValue({});
|
|
57
|
-
(Application as unknown as { container: Container | null }).container = container;
|
|
58
|
-
|
|
59
|
-
expect(() => MethodFacade.callMethod("missing")).toThrow(
|
|
60
|
-
"Method [missing] does not exist on resolved facade instance.",
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("can clear a single resolved instance", () => {
|
|
65
|
-
const container = new Container();
|
|
66
|
-
container.bind("method.target").toConstantValue({
|
|
67
|
-
value: () => "cached",
|
|
68
|
-
});
|
|
69
|
-
(Application as unknown as { container: Container | null }).container = container;
|
|
70
|
-
|
|
71
|
-
expect(MethodFacade.callMethod<string>("value")).toBe("cached");
|
|
72
|
-
Facade.clearResolvedInstance("method.target");
|
|
73
|
-
expect(MethodFacade.callMethod<string>("value")).toBe("cached");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("base facade constructor is invokable through subclass", () => {
|
|
77
|
-
const instance = new ConcreteFacade();
|
|
78
|
-
expect(instance).toBeInstanceOf(ConcreteFacade);
|
|
79
|
-
});
|
|
80
|
-
});
|
package/tests/filesystem.test.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { FileSystem } from "../src/filesystem/filesystem";
|
|
3
|
-
import LocalAdapter, { __localAdapterTesting } from "../src/filesystem/adapters/local";
|
|
4
|
-
|
|
5
|
-
describe("FileSystem domain", () => {
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
(
|
|
8
|
-
globalThis as {
|
|
9
|
-
__iocConfigGlobForTests?: unknown;
|
|
10
|
-
}
|
|
11
|
-
).__iocConfigGlobForTests = undefined;
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("delegates loading to adapter and supports replacing adapter", () => {
|
|
15
|
-
const calls: string[] = [];
|
|
16
|
-
const adapterA = {
|
|
17
|
-
loadConfigItems: (basePath: string) => {
|
|
18
|
-
calls.push(`a:${basePath}`);
|
|
19
|
-
return { app: { name: "A" } };
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
const adapterB = {
|
|
23
|
-
loadConfigItems: (basePath: string) => {
|
|
24
|
-
calls.push(`b:${basePath}`);
|
|
25
|
-
return { app: { name: "B" } };
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const filesystem = new FileSystem(adapterA);
|
|
30
|
-
expect(filesystem.loadConfigItems("./")).toEqual({ app: { name: "A" } });
|
|
31
|
-
|
|
32
|
-
filesystem.setAdapter(adapterB);
|
|
33
|
-
expect(filesystem.loadConfigItems("./src")).toEqual({ app: { name: "B" } });
|
|
34
|
-
expect(calls).toEqual(["a:./", "b:./src"]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("loads config defaults from matching directories", () => {
|
|
38
|
-
(
|
|
39
|
-
globalThis as {
|
|
40
|
-
__iocConfigGlobForTests?: (
|
|
41
|
-
pattern: string | string[],
|
|
42
|
-
options?: { eager?: boolean },
|
|
43
|
-
) => Record<string, unknown>;
|
|
44
|
-
}
|
|
45
|
-
).__iocConfigGlobForTests = () => ({
|
|
46
|
-
"/src/config/app.ts": { default: { name: "App" } },
|
|
47
|
-
"/src/config/cache.ts": { default: { driver: "memory" } },
|
|
48
|
-
"/src/config/no-default.ts": { named: true },
|
|
49
|
-
"/ignored/config/app.ts": { default: { name: "Ignored" } },
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const adapter = new LocalAdapter();
|
|
53
|
-
|
|
54
|
-
expect(adapter.loadConfigItems("./src")).toEqual({
|
|
55
|
-
app: { name: "App" },
|
|
56
|
-
cache: { driver: "memory" },
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("helper functions normalize and match config paths", () => {
|
|
61
|
-
expect(__localAdapterTesting.fileNameWithoutExtension("/src/config/app.ts")).toBe("app");
|
|
62
|
-
expect(__localAdapterTesting.normalizeBasePath("./")).toBe("");
|
|
63
|
-
expect(__localAdapterTesting.normalizeBasePath("./src")).toBe("src");
|
|
64
|
-
expect(__localAdapterTesting.isPathInConfigDirectories("/src/config/app.ts", "./")).toBe(
|
|
65
|
-
true,
|
|
66
|
-
);
|
|
67
|
-
expect(
|
|
68
|
-
__localAdapterTesting.isPathInConfigDirectories(
|
|
69
|
-
"/workspace/src/config/app.ts",
|
|
70
|
-
"/workspace",
|
|
71
|
-
),
|
|
72
|
-
).toBe(true);
|
|
73
|
-
expect(__localAdapterTesting.isPathInConfigDirectories("/tmp/elsewhere/conf/app.ts", "./"))
|
|
74
|
-
.toBe(false);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("returns empty config when no glob implementation is available", () => {
|
|
78
|
-
const adapter = new LocalAdapter();
|
|
79
|
-
expect(adapter.loadConfigItems("./")).toEqual({});
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { Container } from "inversify";
|
|
3
|
-
import React from "react";
|
|
4
|
-
import { renderToString } from "react-dom/server";
|
|
5
|
-
import { ApplicationProvider, useService } from "../src/runtimes/react";
|
|
6
|
-
|
|
7
|
-
describe("React runtime", () => {
|
|
8
|
-
test("ApplicationProvider + useService resolves from container", () => {
|
|
9
|
-
const container = new Container();
|
|
10
|
-
const token = Symbol("token");
|
|
11
|
-
container.bind(token).toConstantValue("resolved");
|
|
12
|
-
|
|
13
|
-
function Probe() {
|
|
14
|
-
const value = useService<string>(token);
|
|
15
|
-
return <div>{value}</div>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const html = renderToString(
|
|
19
|
-
<ApplicationProvider container={container}>
|
|
20
|
-
<Probe />
|
|
21
|
-
</ApplicationProvider>,
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
expect(html).toContain("resolved");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("useService throws when provider is missing", () => {
|
|
28
|
-
function Probe() {
|
|
29
|
-
useService("missing");
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
expect(() => renderToString(<Probe />)).toThrow(
|
|
34
|
-
"Application container is not available in React context.",
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import ServiceProvider, {
|
|
3
|
-
DeferrableServiceProvider,
|
|
4
|
-
} from "../src/support/service-provider";
|
|
5
|
-
import { Container } from "inversify";
|
|
6
|
-
|
|
7
|
-
describe("ServiceProvider base classes", () => {
|
|
8
|
-
test("default register and boot are no-ops", () => {
|
|
9
|
-
const provider = new ServiceProvider();
|
|
10
|
-
const container = new Container();
|
|
11
|
-
|
|
12
|
-
expect(provider.register({ container })).toBeUndefined();
|
|
13
|
-
expect(provider.boot({ container })).toBeUndefined();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("deferrable provider default provides list is empty", () => {
|
|
17
|
-
const provider = new DeferrableServiceProvider();
|
|
18
|
-
const container = new Container();
|
|
19
|
-
expect(provider.register({ container })).toBeUndefined();
|
|
20
|
-
expect(provider.boot({ container })).toBeUndefined();
|
|
21
|
-
expect(provider.provides()).toEqual([]);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { Container } from "inversify";
|
|
3
|
-
import { Application } from "../src/foundation/application";
|
|
4
|
-
import StorageManager from "../src/storage/manager";
|
|
5
|
-
import MemoryStorageAdapter from "../src/storage/adapters/memory";
|
|
6
|
-
import Storage from "../src/support/facades/storage";
|
|
7
|
-
import Facade from "../src/support/facades/facade";
|
|
8
|
-
|
|
9
|
-
describe("Storage facade", () => {
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
Facade.clearResolvedInstances();
|
|
12
|
-
(Application as unknown as { container: Container | null }).container = null;
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("resolves manager and delegates methods", async () => {
|
|
16
|
-
const container = new Container();
|
|
17
|
-
const manager = new StorageManager({ memory: new MemoryStorageAdapter() }, "memory");
|
|
18
|
-
container.bind("storage").toConstantValue(manager);
|
|
19
|
-
container.bind(StorageManager).toConstantValue(manager);
|
|
20
|
-
(Application as unknown as { container: Container | null }).container = container;
|
|
21
|
-
|
|
22
|
-
await Storage.set("name", "ioc");
|
|
23
|
-
expect(await Storage.get("name")).toBe("ioc");
|
|
24
|
-
expect(await Storage.has("name")).toBe(true);
|
|
25
|
-
expect(await Storage.keys()).toEqual(["name"]);
|
|
26
|
-
expect(Storage.drv()).toBe(manager.drv());
|
|
27
|
-
expect(Storage.drv("memory")).toBe(manager.drv("memory"));
|
|
28
|
-
Storage.use("memory");
|
|
29
|
-
await Storage.delete("name");
|
|
30
|
-
expect(await Storage.get("name")).toBeNull();
|
|
31
|
-
await Storage.set("x", 1);
|
|
32
|
-
await Storage.clear();
|
|
33
|
-
expect(await Storage.keys()).toEqual([]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("uses expected facade accessor", () => {
|
|
37
|
-
expect((Storage as unknown as { getFacadeAccessor: () => string }).getFacadeAccessor()).toBe(
|
|
38
|
-
"storage",
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("facade class constructor is defined", () => {
|
|
43
|
-
const instance = new (Storage as unknown as { new (): object })();
|
|
44
|
-
expect(instance).toBeTruthy();
|
|
45
|
-
});
|
|
46
|
-
});
|
package/tests/storage.test.ts
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import ConfigRepository from "../src/config/repository";
|
|
3
|
-
import IndexedDbAdapter from "../src/storage/adapters/indexed-db";
|
|
4
|
-
import LocalStorageAdapter from "../src/storage/adapters/local-storage";
|
|
5
|
-
import MemoryStorageAdapter from "../src/storage/adapters/memory";
|
|
6
|
-
import StorageManager from "../src/storage/manager";
|
|
7
|
-
|
|
8
|
-
class FakeWebStorage implements Storage {
|
|
9
|
-
protected data = new Map<string, string>();
|
|
10
|
-
|
|
11
|
-
get length(): number {
|
|
12
|
-
return this.data.size;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
clear(): void {
|
|
16
|
-
this.data.clear();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
getItem(key: string): string | null {
|
|
20
|
-
return this.data.get(key) ?? null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
key(index: number): string | null {
|
|
24
|
-
const keys = Array.from(this.data.keys());
|
|
25
|
-
return keys[index] ?? null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
removeItem(key: string): void {
|
|
29
|
-
this.data.delete(key);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
setItem(key: string, value: string): void {
|
|
33
|
-
this.data.set(key, value);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type RequestLike<T> = Partial<IDBRequest<T>> & {
|
|
38
|
-
onsuccess: ((this: IDBRequest<T>, ev: Event) => unknown) | null;
|
|
39
|
-
onerror: ((this: IDBRequest<T>, ev: Event) => unknown) | null;
|
|
40
|
-
onupgradeneeded?: ((this: IDBOpenDBRequest, ev: IDBVersionChangeEvent) => unknown) | null;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function makeRequest<T>(resolveValue: () => T, shouldFail = false): IDBRequest<T> {
|
|
44
|
-
const request: RequestLike<T> = {
|
|
45
|
-
onsuccess: null,
|
|
46
|
-
onerror: null,
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
queueMicrotask(() => {
|
|
50
|
-
if (shouldFail) {
|
|
51
|
-
request.error = new Error("fail") as unknown as DOMException;
|
|
52
|
-
request.onerror?.call(request as IDBRequest<T>, new Event("error"));
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
request.result = resolveValue();
|
|
57
|
-
request.onsuccess?.call(request as IDBRequest<T>, new Event("success"));
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
return request as IDBRequest<T>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function createIndexedDbFactory(options: { failOpen?: boolean; failGet?: boolean } = {}) {
|
|
64
|
-
const items = new Map<string, unknown>();
|
|
65
|
-
let hasStore = false;
|
|
66
|
-
|
|
67
|
-
const store: Partial<IDBObjectStore> = {
|
|
68
|
-
get: (key: IDBValidKey) => makeRequest(() => items.get(String(key)), options.failGet),
|
|
69
|
-
put: (value: unknown, key?: IDBValidKey) =>
|
|
70
|
-
makeRequest(() => {
|
|
71
|
-
items.set(String(key), value);
|
|
72
|
-
return key as IDBValidKey;
|
|
73
|
-
}),
|
|
74
|
-
count: (key?: IDBValidKey | IDBKeyRange) =>
|
|
75
|
-
makeRequest(() => (items.has(String(key)) ? 1 : 0)),
|
|
76
|
-
delete: (key: IDBValidKey | IDBKeyRange) =>
|
|
77
|
-
makeRequest(() => {
|
|
78
|
-
items.delete(String(key));
|
|
79
|
-
return undefined;
|
|
80
|
-
}),
|
|
81
|
-
clear: () =>
|
|
82
|
-
makeRequest(() => {
|
|
83
|
-
items.clear();
|
|
84
|
-
return undefined;
|
|
85
|
-
}),
|
|
86
|
-
getAllKeys: () => makeRequest(() => Array.from(items.keys()) as Array<IDBValidKey>),
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const db: Partial<IDBDatabase> = {
|
|
90
|
-
objectStoreNames: {
|
|
91
|
-
contains: (name: string) => hasStore && name === "kv",
|
|
92
|
-
item: () => null,
|
|
93
|
-
length: 0,
|
|
94
|
-
[Symbol.iterator]: function* iterator() {},
|
|
95
|
-
} as DOMStringList,
|
|
96
|
-
createObjectStore: () => {
|
|
97
|
-
hasStore = true;
|
|
98
|
-
return store as IDBObjectStore;
|
|
99
|
-
},
|
|
100
|
-
transaction: () =>
|
|
101
|
-
({
|
|
102
|
-
objectStore: () => store as IDBObjectStore,
|
|
103
|
-
}) as IDBTransaction,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const factory: Pick<IDBFactory, "open"> = {
|
|
107
|
-
open: () => {
|
|
108
|
-
const request: RequestLike<IDBDatabase> = {
|
|
109
|
-
onsuccess: null,
|
|
110
|
-
onerror: null,
|
|
111
|
-
onupgradeneeded: null,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
queueMicrotask(() => {
|
|
115
|
-
if (options.failOpen) {
|
|
116
|
-
request.error = new Error("open failed") as unknown as DOMException;
|
|
117
|
-
request.onerror?.call(request as IDBOpenDBRequest, new Event("error"));
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
request.result = db as IDBDatabase;
|
|
122
|
-
request.onupgradeneeded?.call(
|
|
123
|
-
request as IDBOpenDBRequest,
|
|
124
|
-
new Event("upgradeneeded") as IDBVersionChangeEvent,
|
|
125
|
-
);
|
|
126
|
-
request.onsuccess?.call(request as IDBOpenDBRequest, new Event("success"));
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
return request as IDBOpenDBRequest;
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
return { factory, items };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
describe("Storage adapters and manager", () => {
|
|
137
|
-
test("memory adapter reads/writes/deletes/clears", async () => {
|
|
138
|
-
const adapter = new MemoryStorageAdapter();
|
|
139
|
-
|
|
140
|
-
await adapter.set("a", 1);
|
|
141
|
-
expect(await adapter.get<number>("a")).toBe(1);
|
|
142
|
-
expect(await adapter.has("a")).toBe(true);
|
|
143
|
-
expect(await adapter.keys()).toEqual(["a"]);
|
|
144
|
-
await adapter.delete("a");
|
|
145
|
-
expect(await adapter.get("a")).toBeNull();
|
|
146
|
-
await adapter.set("b", 2);
|
|
147
|
-
await adapter.clear();
|
|
148
|
-
expect(await adapter.keys()).toEqual([]);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("local storage adapter serializes values", async () => {
|
|
152
|
-
const storage = new FakeWebStorage();
|
|
153
|
-
const adapter = new LocalStorageAdapter(storage);
|
|
154
|
-
|
|
155
|
-
await adapter.set("name", { v: "ioc" });
|
|
156
|
-
expect(await adapter.get<{ v: string }>("name")).toEqual({ v: "ioc" });
|
|
157
|
-
expect(await adapter.has("name")).toBe(true);
|
|
158
|
-
expect(await adapter.keys()).toEqual(["name"]);
|
|
159
|
-
await adapter.delete("name");
|
|
160
|
-
expect(await adapter.get("name")).toBeNull();
|
|
161
|
-
await adapter.set("x", 1);
|
|
162
|
-
await adapter.clear();
|
|
163
|
-
expect(await adapter.keys()).toEqual([]);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("indexed db adapter uses indexeddb when available", async () => {
|
|
167
|
-
const { factory, items } = createIndexedDbFactory();
|
|
168
|
-
const adapter = new IndexedDbAdapter({ factory, name: "ioc-test" });
|
|
169
|
-
|
|
170
|
-
await adapter.set("k", { n: 1 });
|
|
171
|
-
expect(items.get("k")).toEqual({ n: 1 });
|
|
172
|
-
expect(await adapter.get<{ n: number }>("k")).toEqual({ n: 1 });
|
|
173
|
-
expect(await adapter.has("k")).toBe(true);
|
|
174
|
-
expect(await adapter.keys()).toEqual(["k"]);
|
|
175
|
-
await adapter.delete("k");
|
|
176
|
-
expect(await adapter.get("k")).toBeNull();
|
|
177
|
-
await adapter.set("z", true);
|
|
178
|
-
await adapter.clear();
|
|
179
|
-
expect(await adapter.keys()).toEqual([]);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test("indexed db adapter falls back to memory when indexeddb is unavailable or fails", async () => {
|
|
183
|
-
const fallback = new MemoryStorageAdapter();
|
|
184
|
-
const unavailable = new IndexedDbAdapter({ factory: null, fallback });
|
|
185
|
-
|
|
186
|
-
await unavailable.set("a", 1);
|
|
187
|
-
expect(await unavailable.get<number>("a")).toBe(1);
|
|
188
|
-
expect(await unavailable.has("a")).toBe(true);
|
|
189
|
-
expect(await unavailable.keys()).toEqual(["a"]);
|
|
190
|
-
await unavailable.delete("a");
|
|
191
|
-
expect(await unavailable.has("a")).toBe(false);
|
|
192
|
-
await unavailable.set("b", 2);
|
|
193
|
-
await unavailable.clear();
|
|
194
|
-
expect(await unavailable.keys()).toEqual([]);
|
|
195
|
-
|
|
196
|
-
const failedOpenFactory: Pick<IDBFactory, "open"> = {
|
|
197
|
-
open: () => null as unknown as IDBOpenDBRequest,
|
|
198
|
-
};
|
|
199
|
-
const failedOpen = new IndexedDbAdapter({ factory: failedOpenFactory, fallback });
|
|
200
|
-
await failedOpen.set("x", 42);
|
|
201
|
-
expect(await failedOpen.get<number>("x")).toBe(42);
|
|
202
|
-
|
|
203
|
-
const { factory: failOpenFactory } = createIndexedDbFactory({ failOpen: true });
|
|
204
|
-
const failedOpenEvent = new IndexedDbAdapter({ factory: failOpenFactory, fallback });
|
|
205
|
-
expect(await failedOpenEvent.get("none")).toBeNull();
|
|
206
|
-
|
|
207
|
-
const { factory: failGetFactory } = createIndexedDbFactory({ failGet: true });
|
|
208
|
-
const failedGet = new IndexedDbAdapter({ factory: failGetFactory, fallback });
|
|
209
|
-
await failedGet.set("y", 7);
|
|
210
|
-
expect(await failedGet.get<number>("y")).toBeNull();
|
|
211
|
-
|
|
212
|
-
const rejectingFallback = {
|
|
213
|
-
get: async () => null,
|
|
214
|
-
set: async () => {
|
|
215
|
-
throw new Error("set failed");
|
|
216
|
-
},
|
|
217
|
-
has: async () => false,
|
|
218
|
-
delete: async () => {},
|
|
219
|
-
clear: async () => {},
|
|
220
|
-
keys: async () => [],
|
|
221
|
-
};
|
|
222
|
-
const rejected = new IndexedDbAdapter({ factory: null, fallback: rejectingFallback });
|
|
223
|
-
await expect(rejected.set("bad", 1)).rejects.toThrow("set failed");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("storage manager chooses and switches drivers", async () => {
|
|
227
|
-
const memory = new MemoryStorageAdapter();
|
|
228
|
-
const alt = new MemoryStorageAdapter();
|
|
229
|
-
const manager = new StorageManager({ memory, alt }, "memory");
|
|
230
|
-
|
|
231
|
-
await manager.set("k", 1);
|
|
232
|
-
expect(await manager.get("k")).toBe(1);
|
|
233
|
-
manager.use("alt");
|
|
234
|
-
expect(manager.drv()).toBe(alt);
|
|
235
|
-
expect(() => manager.drv("missing")).toThrow("Storage driver [missing] is not defined.");
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("storage manager builds defaults from config", () => {
|
|
239
|
-
const originalWindow = (globalThis as { window?: unknown }).window;
|
|
240
|
-
const originalIndexedDb = (globalThis as { indexedDB?: unknown }).indexedDB;
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
(globalThis as { window?: unknown }).window = {
|
|
244
|
-
localStorage: new FakeWebStorage(),
|
|
245
|
-
};
|
|
246
|
-
(globalThis as { indexedDB?: unknown }).indexedDB = createIndexedDbFactory().factory;
|
|
247
|
-
|
|
248
|
-
const manager = StorageManager.fromConfig(
|
|
249
|
-
new ConfigRepository({
|
|
250
|
-
storage: {
|
|
251
|
-
driver: "local",
|
|
252
|
-
},
|
|
253
|
-
}),
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
expect(manager.drv()).toBeTruthy();
|
|
257
|
-
expect(manager.drv("indexed")).toBeTruthy();
|
|
258
|
-
expect(manager.drv("memory")).toBeTruthy();
|
|
259
|
-
} finally {
|
|
260
|
-
(globalThis as { window?: unknown }).window = originalWindow;
|
|
261
|
-
(globalThis as { indexedDB?: unknown }).indexedDB = originalIndexedDb;
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
});
|