Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 70 additions & 71 deletions bun.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wirebox",
"version": "1.0.0-alpha.2",
"version": "0.6.0",
"type": "module",
"module": "./src/index.js",
"description": "A simple but flexible dependency injection library.",
Expand Down Expand Up @@ -33,10 +33,10 @@
"./package.json": "./package.json"
},
"devDependencies": {
"@biomejs/biome": "^2.3.5",
"@types/bun": "^1.3.2",
"tsdown": "^0.16.4",
"typedoc": "^0.28.14",
"@biomejs/biome": "^2.3.14",
"@types/bun": "^1.3.8",
"tsdown": "^0.20.3",
"typedoc": "^0.28.16",
"typedoc-github-theme": "^0.3.1",
"typescript": "^5.9.3"
}
Expand Down
129 changes: 85 additions & 44 deletions src/circuit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
AlreadyInitializedError,
AsyncDependencyError,
InvalidProvidableError,
NoCircuitLinkError,
NoCircuitContextError,
UnwiredError,
} from "./errors.ts";
import {
Expand All @@ -13,7 +13,7 @@ import {
} from "./provider/provider.ts";
import type { Class, Context, ResolvedInstance, Wrapped } from "./types.ts";

let currentCircuit: Circuit | null = null;
let currentContext: Context | null = null;

/**
* A circuit is a container which is responsible for managing the instances by holding and initializing them.
Expand Down Expand Up @@ -66,8 +66,8 @@ export class Circuit {
// get the class definition
const definition = WireDefinition.from(target);

// if the target not resolved yet and no meta is available, throw an error
if (!definition) throw new UnwiredError(target);
// if the target not resolved yet and no valid definition is available, throw an error
if (!definition || !definition.isValid()) throw new UnwiredError(target);

// if the class is a singleton, forward the tap to the singleton circuit
if (definition.singleton && definition.singleton !== this)
Expand All @@ -77,8 +77,12 @@ export class Circuit {
if (this.#asyncInitializers.has(target))
throw new AsyncDependencyError(target, true);

// if target is async and not initialized, throw an error
if (definition.async) throw new AsyncDependencyError(target, false);
// if target has an async preconstruct, throw an error
if (definition.preconstructAsync)
throw new AsyncDependencyError(target, false);

// if target has setup, throw an error
if (definition.setup) throw new AsyncDependencyError(target, false);

// resolve the inputs
const inputs = this.#resolveDependencies(
Expand All @@ -87,25 +91,19 @@ export class Circuit {
);

// initialize the class either with the preconstruct if available or with the constructor
const instance = this.#runWithCircuit(() =>
definition.preconstruct
? definition.preconstruct(inputs, context)
: Reflect.construct(target, inputs),
const instance = this.#runWithContext(
() =>
definition.preconstruct
? definition.preconstruct(inputs, context)
: Reflect.construct(target, inputs),
context,
);

// check if the class is async
if (instance instanceof Promise) {
// handle the promise and save it to prevent multiple async initializations
this.#handlePromise(target, instance);

throw new AsyncDependencyError(target, true);
} else {
// save the instance to prevent multiple initializations
this.#instances.set(target, instance);
// save the instance to prevent multiple initializations
this.#instances.set(target, instance);

// resolve and return the instance
return instance;
}
// resolve and return the instance
return instance;
}

#resolveAsync(target: Class, context: Context<Class>): Promise<unknown> {
Expand All @@ -124,29 +122,54 @@ export class Circuit {
// get the class definition
const definition = WireDefinition.from(target);

// if the target not resolved yet and no definition is available, throw an error
if (!definition) throw new UnwiredError(target);
// if the target not resolved yet and no valid definition is available, throw an error
if (!definition || !definition.isValid()) throw new UnwiredError(target);

// if the class is a singleton, forward the tap to the singleton circuit
if (definition.singleton && definition.singleton !== this)
return definition.singleton.#resolveAsync(target, context);

// resolve the inputs and initialize the class, with either the initializer or the constructor
const initializer = this.resolveDependenciesAsync(
const initializer = this.#resolveDependenciesAsync(
definition.dependencies?.() ?? [],
definition.preloads?.() ?? [],
context,
).then((inputs) =>
this.#runWithCircuit(() =>
definition.preconstruct
? definition.preconstruct(inputs, context)
: Reflect.construct(target, inputs),
),
);
).then(async (inputs) => {
const instancePromise = this.#runWithContext(
() =>
// use preconstructAsync if available
definition.preconstructAsync
? definition.preconstructAsync(inputs, context)
: // then use preconstruct if available
definition.preconstruct
? definition.preconstruct(inputs, context)
: // otherwise use the constructor
Reflect.construct(target, inputs),
context,
);

const instance = await instancePromise;

// if there is no setup, return the instance directly
if (!definition.setup) return instance;

// run the setup
let setupPromise = definition.setup.bind(instance)();

// if the setup result is a function, call it to get the actual promise
if (typeof setupPromise === "function") {
setupPromise = setupPromise.bind(instance)();
}

// wait for the setup to finish
await setupPromise;

return instance;
});

// handle the promise and save it to prevent multiple async initializations
// and return the promise
return this.#handlePromise(target, initializer);
return this.#handlePromise(target, initializer, context);
}

/**
Expand Down Expand Up @@ -242,8 +265,11 @@ export class Circuit {
// if no definition is available, return false
if (!definition) return false;

// if target has async initializer, return true
if (definition.async) return true;
// if target has async preconstruct, return true
if (definition.preconstructAsync) return true;

// if target has setup, return true
if (definition.setup) return true;

const ctx = this.#createContext(target);

Expand All @@ -261,20 +287,21 @@ export class Circuit {
return providerInfo.async ?? false;
}

#runWithCircuit<T>(fn: () => T): T {
const previousCircuit = currentCircuit;
#runWithContext<T>(fn: () => T, context: Context): T {
const previousContext = currentContext;

try {
currentCircuit = this;
currentContext = context;
return fn();
} finally {
currentCircuit = previousCircuit;
currentContext = previousContext;
}
}

#handlePromise(
target: Class,
initializer: Promise<unknown>,
context: Context,
): Promise<unknown> {
const wrapped = initializer
.then((result) => {
Expand All @@ -283,7 +310,7 @@ export class Circuit {

const instance =
typeof result === "function"
? this.#runWithCircuit(() => result())
? this.#runWithContext(() => result(), context)
: result;

this.#instances.set(target, instance);
Expand All @@ -308,7 +335,7 @@ export class Circuit {
});
}

async resolveDependenciesAsync(
async #resolveDependenciesAsync(
dependencies: readonly Class[],
preloads: readonly Class[],
context: Context<Class>,
Expand Down Expand Up @@ -401,11 +428,25 @@ export const tapAsync = <TTarget extends Class>(
return circuit.tapAsync(target);
};

/**
* @category Core
*/
export const getContext = (): Context => {
const context = currentContext;
if (!context) throw new NoCircuitContextError();
return context;
};

/**
* @category Core
*/
export const getCircuit = (): Circuit => {
return getContext().circuit;
};

/**
* @category Core
*/
export const link = <T extends Class>(target: T): ResolvedInstance<T> => {
const circuit = currentCircuit;
if (!circuit) throw new NoCircuitLinkError(target);
return circuit.tap(target);
return getCircuit().tap(target);
};
Loading