๐Ÿ“ฆ hediet / vscode-observables

๐Ÿ“„ di.tsx ยท 88 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88import { createContext, useContext, ReactNode, Context } from "react";
import { IPropertyTransformerFactory, IReadableObj, Readable } from "./IPropertyTransformer";

// =============================================================================
// Service Key
// =============================================================================

export interface ServiceKey<T> {
    readonly _brand: T;
    readonly id: symbol;
    readonly name: string;
}

export function createServiceKey<T>(name: string): ServiceKey<T> {
    return { _brand: undefined as T, id: Symbol(name), name };
}

// =============================================================================
// DI Container
// =============================================================================

export class DIContainer {
    private readonly _services = new Map<symbol, unknown>();
    private readonly _parent: DIContainer | null;

    constructor(parent: DIContainer | null = null) {
        this._parent = parent;
    }

    register<T>(key: ServiceKey<T>, service: T): this {
        this._services.set(key.id, service);
        return this;
    }

    get<T>(key: ServiceKey<T>): T {
        const service = this._services.get(key.id);
        if (service !== undefined) return service as T;
        if (this._parent) return this._parent.get(key);
        throw new Error(`Service "${key.name}" not registered`);
    }

    has<T>(key: ServiceKey<T>): boolean {
        return this._services.has(key.id) || (this._parent?.has(key) ?? false);
    }

    createChild(): DIContainer {
        return new DIContainer(this);
    }
}

// =============================================================================
// React Context
// =============================================================================

export const DIContext: Context<DIContainer | null> = createContext<DIContainer | null>(null);

export function DIProvider({ container, children }: { container: DIContainer; children: ReactNode }): ReactNode {
    return <DIContext.Provider value={container}>{children}</DIContext.Provider>;
}

export function useDIContainer(): DIContainer {
    const container = useContext(DIContext);
    if (!container) throw new Error("DIProvider not found");
    return container;
}

// =============================================================================
// inject() - Property Transformer for DI
// =============================================================================

class InjectTransformerFactory<T> implements IPropertyTransformerFactory<never, T> {
    readonly _requiredContext = DIContext as Context<unknown>;

    constructor(public readonly serviceKey: ServiceKey<T>) { }

    create(_readable: Readable<never>, contextValue: unknown): IReadableObj<T> {
        const container = contextValue as DIContainer | null;
        if (!container) throw new Error(`inject(${this.serviceKey.name}): DIProvider not found`);
        const service = container.get(this.serviceKey);
        return { read: () => service };
    }
}

/** Inject a service from DIContainer into a ViewModel property */
export function inject<T>(key: ServiceKey<T>): IPropertyTransformerFactory<never, T> & { _requiredContext: Context<unknown> } {
    return new InjectTransformerFactory(key);
}