@raubjo/architect 0.5.1
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 +860 -0
- package/package.json +121 -0
- package/src/cache/cache.ts +46 -0
- package/src/cache/contract.ts +9 -0
- package/src/cache/manager.ts +110 -0
- package/src/cache/provider.ts +11 -0
- package/src/config/contract.ts +63 -0
- package/src/config/discovery.ts +99 -0
- package/src/config/env.global.d.ts +6 -0
- package/src/config/env.ts +68 -0
- package/src/config/index.ts +5 -0
- package/src/config/provider.ts +17 -0
- package/src/config/repository.ts +164 -0
- package/src/container/adapters/builtin.ts +323 -0
- package/src/container/contract.ts +43 -0
- package/src/container/runtime.ts +29 -0
- package/src/events/bus.ts +174 -0
- package/src/events/concerns/dispatchable.ts +10 -0
- package/src/events/provider.ts +9 -0
- package/src/events/types.ts +9 -0
- package/src/foundation/application.ts +136 -0
- package/src/foundation/current-application.ts +20 -0
- package/src/index.ts +58 -0
- package/src/log/contract.ts +21 -0
- package/src/log/drivers/console.ts +54 -0
- package/src/log/drivers/null.ts +23 -0
- package/src/log/drivers/stack.ts +46 -0
- package/src/log/manager.ts +76 -0
- package/src/log/provider.ts +11 -0
- package/src/react.ts +2 -0
- package/src/renderers/adapters/react.tsx +25 -0
- package/src/renderers/adapters/solid.tsx +26 -0
- package/src/renderers/adapters/svelte.ts +73 -0
- package/src/renderers/adapters/vue.ts +22 -0
- package/src/renderers/contract.ts +12 -0
- package/src/runtimes/react.tsx +81 -0
- package/src/runtimes/solid.tsx +47 -0
- package/src/runtimes/svelte.ts +17 -0
- package/src/runtimes/vue.ts +34 -0
- package/src/solid.ts +2 -0
- package/src/store/adapters/contract.ts +11 -0
- package/src/store/adapters/indexed-db.ts +187 -0
- package/src/store/adapters/local-storage.ts +48 -0
- package/src/store/adapters/memory.ts +35 -0
- package/src/store/manager.ts +68 -0
- package/src/store/provider.ts +10 -0
- package/src/store/store.ts +1 -0
- package/src/support/arr.ts +372 -0
- package/src/support/collection.ts +889 -0
- package/src/support/facades/cache.ts +6 -0
- package/src/support/facades/config.ts +6 -0
- package/src/support/facades/event.ts +6 -0
- package/src/support/facades/facade.ts +146 -0
- package/src/support/facades/index.ts +5 -0
- package/src/support/facades/log.ts +6 -0
- package/src/support/facades/store.ts +6 -0
- package/src/support/fluent.ts +56 -0
- package/src/support/globals.ts +8 -0
- package/src/support/lazy-collection.ts +341 -0
- package/src/support/manager.ts +53 -0
- package/src/support/num.ts +50 -0
- package/src/support/pipeline.ts +29 -0
- package/src/support/service-provider.ts +19 -0
- package/src/support/str.ts +682 -0
- package/src/svelte.ts +2 -0
- package/src/types/peer-deps.d.ts +10 -0
- package/src/vue.ts +2 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactElement } from "react"
|
|
2
|
+
import { createElement } from "react"
|
|
3
|
+
import ReactDOM from "react-dom/client"
|
|
4
|
+
import { ApplicationProvider } from "../../runtimes/react"
|
|
5
|
+
import type { Cleanup } from "../../support/service-provider"
|
|
6
|
+
import type Contract from "../contract"
|
|
7
|
+
import type { RendererContext } from "../contract"
|
|
8
|
+
|
|
9
|
+
type ReactRootComponent = () => ReactElement | null
|
|
10
|
+
|
|
11
|
+
export default class ReactRenderer implements Contract {
|
|
12
|
+
render({ RootComponent, container, rootElementId }: RendererContext): Cleanup {
|
|
13
|
+
const mountNode = document.getElementById(rootElementId)
|
|
14
|
+
if (!mountNode) {
|
|
15
|
+
throw new Error(`Missing mount node #${rootElementId}.`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const root = ReactDOM.createRoot(mountNode)
|
|
19
|
+
root.render(
|
|
20
|
+
createElement(ApplicationProvider, { container }, createElement(RootComponent as ReactRootComponent)),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return () => root.unmount()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createComponent, type JSX } from "solid-js"
|
|
2
|
+
import { render } from "solid-js/web"
|
|
3
|
+
import { ApplicationProvider } from "../../runtimes/solid"
|
|
4
|
+
import type { Cleanup } from "../../support/service-provider"
|
|
5
|
+
import type Contract from "../contract"
|
|
6
|
+
import type { RendererContext } from "../contract"
|
|
7
|
+
|
|
8
|
+
type SolidRootComponent = () => JSX.Element
|
|
9
|
+
|
|
10
|
+
export default class SolidRenderer implements Contract {
|
|
11
|
+
render({ RootComponent, container, rootElementId }: RendererContext): Cleanup {
|
|
12
|
+
const mountNode = document.getElementById(rootElementId)
|
|
13
|
+
if (!mountNode) {
|
|
14
|
+
throw new Error(`Missing mount node #${rootElementId}.`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return render(
|
|
18
|
+
() =>
|
|
19
|
+
createComponent(ApplicationProvider, {
|
|
20
|
+
container,
|
|
21
|
+
children: () => (RootComponent as SolidRootComponent)(),
|
|
22
|
+
}),
|
|
23
|
+
mountNode,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mount, unmount } from "svelte"
|
|
2
|
+
import type { Cleanup } from "../../support/service-provider"
|
|
3
|
+
import type Contract from "../contract"
|
|
4
|
+
import type { RendererContext } from "../contract"
|
|
5
|
+
|
|
6
|
+
type SvelteComponentInstance = {
|
|
7
|
+
$destroy?: () => void
|
|
8
|
+
destroy?: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type SvelteComponentConstructor = new (options: {
|
|
12
|
+
target: Element
|
|
13
|
+
props?: Record<string, unknown>
|
|
14
|
+
}) => SvelteComponentInstance
|
|
15
|
+
|
|
16
|
+
type SvelteMount = (component: unknown, options: { target: Element; props?: Record<string, unknown> }) => unknown
|
|
17
|
+
type SvelteUnmount = (instance: unknown) => void | Promise<void>
|
|
18
|
+
|
|
19
|
+
export default class SvelteRenderer implements Contract {
|
|
20
|
+
render({ RootComponent, container, rootElementId }: RendererContext): Cleanup {
|
|
21
|
+
const mountNode = document.getElementById(rootElementId)
|
|
22
|
+
if (!mountNode) {
|
|
23
|
+
throw new Error(`Missing mount node #${rootElementId}.`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = { container }
|
|
27
|
+
const modernCleanup = tryRenderModernSvelte(RootComponent, mountNode, props)
|
|
28
|
+
|
|
29
|
+
return modernCleanup ?? renderLegacySvelte(RootComponent, mountNode, props)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function tryRenderModernSvelte(
|
|
34
|
+
RootComponent: unknown,
|
|
35
|
+
mountNode: Element,
|
|
36
|
+
props: Record<string, unknown>,
|
|
37
|
+
): Cleanup | null {
|
|
38
|
+
const svelteMount = mount as unknown as SvelteMount | undefined
|
|
39
|
+
const svelteUnmount = unmount as unknown as SvelteUnmount | undefined
|
|
40
|
+
|
|
41
|
+
if (typeof svelteMount !== "function" || typeof svelteUnmount !== "function") {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const instance = svelteMount(RootComponent, {
|
|
47
|
+
target: mountNode,
|
|
48
|
+
props,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
void svelteUnmount(instance)
|
|
53
|
+
}
|
|
54
|
+
} catch (_error) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderLegacySvelte(RootComponent: unknown, mountNode: Element, props: Record<string, unknown>): Cleanup {
|
|
60
|
+
const Component = RootComponent as SvelteComponentConstructor
|
|
61
|
+
const instance = new Component({ target: mountNode, props })
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
if (typeof instance.$destroy === "function") {
|
|
65
|
+
instance.$destroy()
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof instance.destroy === "function") {
|
|
70
|
+
instance.destroy()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Component, createApp } from "vue"
|
|
2
|
+
import { containerKey } from "../../runtimes/vue"
|
|
3
|
+
import type { Cleanup } from "../../support/service-provider"
|
|
4
|
+
import type Contract from "../contract"
|
|
5
|
+
import type { RendererContext } from "../contract"
|
|
6
|
+
|
|
7
|
+
type VueRootComponent = Component
|
|
8
|
+
|
|
9
|
+
export default class VueRenderer implements Contract {
|
|
10
|
+
render({ RootComponent, container, rootElementId }: RendererContext): Cleanup {
|
|
11
|
+
const mountNode = document.getElementById(rootElementId)
|
|
12
|
+
if (!mountNode) {
|
|
13
|
+
throw new Error(`Missing mount node #${rootElementId}.`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const app = createApp(RootComponent as VueRootComponent)
|
|
17
|
+
app.provide(containerKey, container)
|
|
18
|
+
app.mount(mountNode)
|
|
19
|
+
|
|
20
|
+
return () => app.unmount()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Cleanup, ServiceProviderContext } from "../support/service-provider"
|
|
2
|
+
|
|
3
|
+
export type RootComponent = unknown
|
|
4
|
+
|
|
5
|
+
export type RendererContext = ServiceProviderContext & {
|
|
6
|
+
RootComponent: RootComponent
|
|
7
|
+
rootElementId: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default interface Contract {
|
|
11
|
+
render(context: RendererContext): void | Cleanup
|
|
12
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext, useEffect, useMemo, useRef, useState } from "react"
|
|
2
|
+
import type { ContainerContract, ContainerIdentifier } from "../container/contract"
|
|
3
|
+
import type { Application } from "../foundation/application"
|
|
4
|
+
|
|
5
|
+
const Context = createContext<ContainerContract | null>(null)
|
|
6
|
+
|
|
7
|
+
type ApplicationProviderProps = {
|
|
8
|
+
container: ContainerContract
|
|
9
|
+
children?: ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ApplicationProvider({ container, children }: ApplicationProviderProps) {
|
|
13
|
+
return <Context.Provider value={container}>{children}</Context.Provider>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ContextProviderProps = {
|
|
17
|
+
application?: Application
|
|
18
|
+
container?: ContainerContract
|
|
19
|
+
fallback?: ReactNode
|
|
20
|
+
children?: ReactNode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ContextProvider({ application, container, fallback = null, children }: ContextProviderProps) {
|
|
24
|
+
if (!application && !container) {
|
|
25
|
+
throw new Error("ContextProvider requires either `application` or `container`.")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const externalRuntime = useMemo(() => (container ? { container, stop: () => {} } : null), [container])
|
|
29
|
+
const [runtime, setRuntime] = useState<{ container: ContainerContract; stop: () => void } | null>(externalRuntime)
|
|
30
|
+
|
|
31
|
+
const stopRef = useRef<null | (() => void)>(null)
|
|
32
|
+
const startedRef = useRef(false)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (externalRuntime) {
|
|
36
|
+
setRuntime(externalRuntime)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (startedRef.current) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
startedRef.current = true
|
|
45
|
+
|
|
46
|
+
const running = application?.run()
|
|
47
|
+
stopRef.current = running.stop
|
|
48
|
+
setRuntime(running)
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
stopRef.current?.()
|
|
52
|
+
stopRef.current = null
|
|
53
|
+
startedRef.current = false
|
|
54
|
+
setRuntime(null)
|
|
55
|
+
}
|
|
56
|
+
}, [application, externalRuntime])
|
|
57
|
+
|
|
58
|
+
if (!runtime) {
|
|
59
|
+
return <>{fallback}</>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return <ApplicationProvider container={runtime.container}>{children}</ApplicationProvider>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useService<T>(identifier: ContainerIdentifier<T>): T {
|
|
66
|
+
const container = useContext(Context)
|
|
67
|
+
if (!container) {
|
|
68
|
+
throw new Error("You must use `useService` inside the Application Context.")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return container.make<T>(identifier)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useContainer(): ContainerContract {
|
|
75
|
+
const container = useContext(Context)
|
|
76
|
+
if (!container) {
|
|
77
|
+
throw new Error("You must use `useContainer` inside the Application Context.")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return container
|
|
81
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createComponent, createContext, type JSX, onCleanup, useContext } from "solid-js"
|
|
2
|
+
import type { ContainerContract, ContainerIdentifier } from "../container/contract"
|
|
3
|
+
import type { Application } from "../foundation/application"
|
|
4
|
+
|
|
5
|
+
const ContainerContext = createContext<ContainerContract | null>(null)
|
|
6
|
+
|
|
7
|
+
type ApplicationProviderProps = {
|
|
8
|
+
container: ContainerContract
|
|
9
|
+
children?: JSX.Element | (() => JSX.Element)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ApplicationProvider(props: ApplicationProviderProps) {
|
|
13
|
+
return createComponent(ContainerContext.Provider, {
|
|
14
|
+
value: props.container,
|
|
15
|
+
children: props.children as never,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ContextProviderProps = {
|
|
20
|
+
application?: Application
|
|
21
|
+
container?: ContainerContract
|
|
22
|
+
children?: JSX.Element | (() => JSX.Element)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ContextProvider(props: ContextProviderProps) {
|
|
26
|
+
if (props.container) {
|
|
27
|
+
return createComponent(ApplicationProvider, { container: props.container, children: props.children as never })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!props.application) {
|
|
31
|
+
throw new Error("ContextProvider requires either `application` or `container`.")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const runtime = props.application.run()
|
|
35
|
+
onCleanup(() => runtime.stop())
|
|
36
|
+
|
|
37
|
+
return createComponent(ApplicationProvider, { container: runtime.container, children: props.children as never })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useService<T>(identifier: ContainerIdentifier<T>): T {
|
|
41
|
+
const container = useContext(ContainerContext)
|
|
42
|
+
if (!container) {
|
|
43
|
+
throw new Error("Application container is not available in Solid context.")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return container.make<T>(identifier)
|
|
47
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getContext, setContext } from "svelte"
|
|
2
|
+
import type { ContainerContract, ContainerIdentifier } from "../container/contract"
|
|
3
|
+
|
|
4
|
+
export const containerKey: unique symbol = Symbol("application.container")
|
|
5
|
+
|
|
6
|
+
export function provideContainer(container: ContainerContract): void {
|
|
7
|
+
setContext(containerKey, container)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useService<T>(identifier: ContainerIdentifier<T>): T {
|
|
11
|
+
const container = getContext<ContainerContract | null>(containerKey) ?? null
|
|
12
|
+
if (!container) {
|
|
13
|
+
throw new Error("Application container is not available in Svelte context.")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return container.make<T>(identifier)
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineComponent, type InjectionKey, inject, onUnmounted, provide } from "vue"
|
|
2
|
+
import type { ContainerContract, ContainerIdentifier } from "../container/contract"
|
|
3
|
+
import type { Application } from "../foundation/application"
|
|
4
|
+
|
|
5
|
+
export const containerKey: InjectionKey<ContainerContract> = Symbol("application.container")
|
|
6
|
+
|
|
7
|
+
export const ContextProvider = defineComponent({
|
|
8
|
+
name: "ArchitectContextProvider",
|
|
9
|
+
props: {
|
|
10
|
+
application: { type: Object as () => Application, required: false },
|
|
11
|
+
container: { type: Object as () => ContainerContract, required: false },
|
|
12
|
+
},
|
|
13
|
+
setup(props, { slots }) {
|
|
14
|
+
if (!props.application && !props.container) {
|
|
15
|
+
throw new Error("ContextProvider requires either `application` or `container`.")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const runtime = props.container ? { container: props.container, stop: () => {} } : props.application?.run()
|
|
19
|
+
|
|
20
|
+
provide(containerKey, runtime.container)
|
|
21
|
+
onUnmounted(() => runtime.stop())
|
|
22
|
+
|
|
23
|
+
return () => slots.default?.() ?? []
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export function useService<T>(identifier: ContainerIdentifier<T>): T {
|
|
28
|
+
const container = inject(containerKey, null)
|
|
29
|
+
if (!container) {
|
|
30
|
+
throw new Error("Application container is not available in Vue context.")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return container.make<T>(identifier)
|
|
34
|
+
}
|
package/src/solid.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface Adapter {
|
|
2
|
+
get(key: string): Promise<unknown>
|
|
3
|
+
get<T>(key: string): Promise<T | null>
|
|
4
|
+
set<T = unknown>(key: string, value: T): Promise<void>
|
|
5
|
+
has(key: string): Promise<boolean>
|
|
6
|
+
delete(key: string): Promise<void>
|
|
7
|
+
clear(): Promise<void>
|
|
8
|
+
keys(): Promise<string[]>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Contract = Adapter
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { Adapter } from "./contract"
|
|
2
|
+
import MemoryStoreAdapter from "./memory"
|
|
3
|
+
|
|
4
|
+
type OpenFactory = Pick<IDBFactory, "open">
|
|
5
|
+
|
|
6
|
+
export default class IndexedDbAdapter implements Adapter {
|
|
7
|
+
protected fallback: Adapter
|
|
8
|
+
protected name: string
|
|
9
|
+
protected factory: OpenFactory | null
|
|
10
|
+
protected dbPromise: Promise<IDBDatabase> | null
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
options: {
|
|
14
|
+
factory?: OpenFactory | null
|
|
15
|
+
name?: string
|
|
16
|
+
fallback?: Adapter
|
|
17
|
+
} = {},
|
|
18
|
+
) {
|
|
19
|
+
this.factory = resolveOpenFactory(options.factory)
|
|
20
|
+
this.name = resolveDatabaseName(options.name)
|
|
21
|
+
this.fallback = resolveFallbackAdapter(options.fallback)
|
|
22
|
+
this.dbPromise = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected req<T>(request: IDBRequest<T>): Promise<T> {
|
|
26
|
+
return new Promise<T>((resolve, reject) => {
|
|
27
|
+
request.onsuccess = () => resolve(request.result)
|
|
28
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed."))
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected openDb(): Promise<IDBDatabase> {
|
|
33
|
+
if (this.dbPromise) {
|
|
34
|
+
return this.dbPromise
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!this.factory) {
|
|
38
|
+
return Promise.reject(new Error("IndexedDB is not available."))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
|
|
42
|
+
const request = this.factory?.open(this.name, 1)
|
|
43
|
+
if (!request) {
|
|
44
|
+
reject(new Error("IndexedDB open request could not be created."))
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
request.onupgradeneeded = () => {
|
|
49
|
+
const db = request.result
|
|
50
|
+
if (!db.objectStoreNames.contains("kv")) {
|
|
51
|
+
db.createObjectStore("kv")
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
request.onsuccess = () => resolve(request.result)
|
|
55
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB database could not be opened."))
|
|
56
|
+
}).catch((error) => {
|
|
57
|
+
this.dbPromise = null
|
|
58
|
+
throw error
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return this.dbPromise
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected async withStore<T>(mode: IDBTransactionMode, action: (store: IDBObjectStore) => Promise<T>): Promise<T> {
|
|
65
|
+
try {
|
|
66
|
+
const db = await this.openDb()
|
|
67
|
+
const tx = db.transaction("kv", mode)
|
|
68
|
+
const store = tx.objectStore("kv")
|
|
69
|
+
return await action(store)
|
|
70
|
+
} catch (_error) {
|
|
71
|
+
return actionFallback(this.fallback, mode, action)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async get(key: string): Promise<unknown>
|
|
76
|
+
async get<T>(key: string): Promise<T | null>
|
|
77
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
78
|
+
return this.withStore("readonly", async (store) => {
|
|
79
|
+
const value = await this.req<unknown>(store.get(key))
|
|
80
|
+
return value === undefined ? null : (value as T)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async set<T = unknown>(key: string, value: T): Promise<void> {
|
|
85
|
+
await this.withStore("readwrite", async (store) => {
|
|
86
|
+
await this.req(store.put(value, key))
|
|
87
|
+
return undefined
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async has(key: string): Promise<boolean> {
|
|
92
|
+
return this.withStore("readonly", async (store) => {
|
|
93
|
+
const count = await this.req<number>(store.count(key))
|
|
94
|
+
return count > 0
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async delete(key: string): Promise<void> {
|
|
99
|
+
await this.withStore("readwrite", async (store) => {
|
|
100
|
+
await this.req(store.delete(key))
|
|
101
|
+
return undefined
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async clear(): Promise<void> {
|
|
106
|
+
await this.withStore("readwrite", async (store) => {
|
|
107
|
+
await this.req(store.clear())
|
|
108
|
+
return undefined
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async keys(): Promise<string[]> {
|
|
113
|
+
return this.withStore("readonly", async (store) => {
|
|
114
|
+
const keys = await this.req<Array<IDBValidKey>>(store.getAllKeys())
|
|
115
|
+
return keys.map(String)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveOpenFactory(factory?: OpenFactory | null): OpenFactory | null {
|
|
121
|
+
return factory ?? globalThis.indexedDB ?? null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveDatabaseName(name?: string): string {
|
|
125
|
+
return name ?? "ioc-store"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveFallbackAdapter(fallback?: Adapter): Adapter {
|
|
129
|
+
return fallback ?? new MemoryStoreAdapter()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function actionFallback<T>(
|
|
133
|
+
fallback: Adapter,
|
|
134
|
+
mode: IDBTransactionMode,
|
|
135
|
+
action: (store: IDBObjectStore) => Promise<T>,
|
|
136
|
+
): Promise<T> {
|
|
137
|
+
// Keep call sites small: map IDB actions to the same short storage contract.
|
|
138
|
+
if (mode === "readonly") {
|
|
139
|
+
const store = createReadonlyProxy(fallback)
|
|
140
|
+
return action(store as unknown as IDBObjectStore)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const store = createReadWriteProxy(fallback)
|
|
144
|
+
return action(store as unknown as IDBObjectStore)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function createReadonlyProxy(fallback: Adapter): Partial<IDBObjectStore> {
|
|
148
|
+
return {
|
|
149
|
+
get: (key: IDBValidKey) => wrapPromiseRequest(fallback.get(String(key))),
|
|
150
|
+
count: (key?: IDBValidKey | IDBKeyRange) =>
|
|
151
|
+
wrapPromiseRequest(fallback.has(String(key as IDBValidKey)).then((exists) => (exists ? 1 : 0))),
|
|
152
|
+
getAllKeys: () => wrapPromiseRequest(fallback.keys().then((keys) => keys as Array<IDBValidKey>)),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function createReadWriteProxy(fallback: Adapter): Partial<IDBObjectStore> {
|
|
157
|
+
return {
|
|
158
|
+
...createReadonlyProxy(fallback),
|
|
159
|
+
put: (value: unknown, key?: IDBValidKey) =>
|
|
160
|
+
wrapPromiseRequest(fallback.set(String(key as IDBValidKey), value)) as unknown as IDBRequest<IDBValidKey>,
|
|
161
|
+
delete: (key: IDBValidKey | IDBKeyRange) =>
|
|
162
|
+
wrapPromiseRequest(fallback.delete(String(key as IDBValidKey))) as unknown as IDBRequest<undefined>,
|
|
163
|
+
clear: () => wrapPromiseRequest(fallback.clear()) as unknown as IDBRequest<undefined>,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function wrapPromiseRequest<T>(promise: Promise<T>): IDBRequest<T> {
|
|
168
|
+
const request = {
|
|
169
|
+
onsuccess: null as ((this: IDBRequest<T>, ev: Event) => unknown) | null,
|
|
170
|
+
onerror: null as ((this: IDBRequest<T>, ev: Event) => unknown) | null,
|
|
171
|
+
result: undefined as T | undefined,
|
|
172
|
+
error: null as DOMException | null,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
promise.then(
|
|
176
|
+
(result) => {
|
|
177
|
+
request.result = result
|
|
178
|
+
request.onsuccess?.call(request as unknown as IDBRequest<T>, new Event("success"))
|
|
179
|
+
},
|
|
180
|
+
(error) => {
|
|
181
|
+
request.error = error as DOMException
|
|
182
|
+
request.onerror?.call(request as unknown as IDBRequest<T>, new Event("error"))
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return request as unknown as IDBRequest<T>
|
|
187
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Adapter } from "./contract"
|
|
2
|
+
|
|
3
|
+
export default class LocalStorageAdapter implements Adapter {
|
|
4
|
+
protected storage: Storage
|
|
5
|
+
|
|
6
|
+
constructor(storage: Storage = window.localStorage) {
|
|
7
|
+
this.storage = storage
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async get(key: string): Promise<unknown>
|
|
11
|
+
async get<T>(key: string): Promise<T | null>
|
|
12
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
13
|
+
const value = this.storage.getItem(key)
|
|
14
|
+
if (value === null) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return JSON.parse(value) as T
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async set<T = unknown>(key: string, value: T): Promise<void> {
|
|
22
|
+
this.storage.setItem(key, JSON.stringify(value))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async has(key: string): Promise<boolean> {
|
|
26
|
+
return this.storage.getItem(key) !== null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async delete(key: string): Promise<void> {
|
|
30
|
+
this.storage.removeItem(key)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async clear(): Promise<void> {
|
|
34
|
+
this.storage.clear()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async keys(): Promise<string[]> {
|
|
38
|
+
const keys: string[] = []
|
|
39
|
+
for (let i = 0; i < this.storage.length; i += 1) {
|
|
40
|
+
const key = this.storage.key(i)
|
|
41
|
+
if (key !== null) {
|
|
42
|
+
keys.push(key)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return keys
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Adapter } from "./contract"
|
|
2
|
+
|
|
3
|
+
export default class MemoryStoreAdapter implements Adapter {
|
|
4
|
+
protected items = new Map<string, unknown>()
|
|
5
|
+
|
|
6
|
+
async get(key: string): Promise<unknown>
|
|
7
|
+
async get<T>(key: string): Promise<T | null>
|
|
8
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
9
|
+
if (!this.items.has(key)) {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return this.items.get(key) as T
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async set<T = unknown>(key: string, value: T): Promise<void> {
|
|
17
|
+
this.items.set(key, value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async has(key: string): Promise<boolean> {
|
|
21
|
+
return this.items.has(key)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async delete(key: string): Promise<void> {
|
|
25
|
+
this.items.delete(key)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async clear(): Promise<void> {
|
|
29
|
+
this.items.clear()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async keys(): Promise<string[]> {
|
|
33
|
+
return Array.from(this.items.keys())
|
|
34
|
+
}
|
|
35
|
+
}
|