npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@caspian-vega/small-vial

v0.5.0

Published

A lightweight, TypeScript-based dependency injection system designed for Astro applications with SSR support

Readme

@caspian-vega/small-vial

Dependency injection for Astro and other Vite-based SSR apps. Decorator registration, an svInject() call usable anywhere, and request-scoped containers.

Vite is required. SSR detection and the dev/test/production branches read import.meta.env, which Vite replaces at build time. Bundlers that do not define it (webpack, Turbopack, esbuild on its own, bare Node ESM) are out of scope: the first svInject(...) throws there. Astro, Vite SSR and Vitest are the supported hosts.

Part of caspian-vega-astro-libs.

Table of contents

Install

npm install @caspian-vega/small-vial
# or
pnpm add @caspian-vega/small-vial

Usage

import { type PostConstructable, Service, svInject } from '@caspian-vega/small-vial';

@Service()
class UserService implements PostConstructable {
    postConstruct() {}

    getUser(id: string) {
        return this.users.get(id);
    }
}

@Service()
class AuthService {
    private userService = svInject(UserService);

    authenticate(credentials: Credentials) {
        return this.userService.getUser(credentials.userId);
    }
}

const auth = svInject(AuthService);

Constructor injection uses the same call:

@Service()
class AuthService {
    constructor(private userService = svInject(UserService)) {}
}

Setup

Decorators

The library uses canonical TC39 stage-3 decorators (2023-11), not the legacy TypeScript pipeline. All five decorators are class decorators, so the toolchain must transform them before the runtime sees them.

  • Astro 6: transforms through esbuild, no extra setup.
  • Astro 7 and Vite 8: build with OXC, which regressed decorator support. OXC does not lower 2023-11 class decorators yet, tracked in oxc-project/oxc#9170. Decorated files have to run through @babel/plugin-proposal-decorators first, or every @Service() is dropped and nothing registers. The Babel step is a workaround for that regression, not a permanent requirement.

The transform ships with the package, so the Babel wiring is one integration entry. Treat it as temporary: once decorator support lands in OXC and ships through Vite into Astro, the Babel pass and its optional peers can be dropped and smallVial({ decorators: false }) is enough.

Install the optional peers it drives:

npm install --save-dev @babel/core @babel/plugin-proposal-decorators @rolldown/plugin-babel
# or
pnpm add -D @babel/core @babel/plugin-proposal-decorators @rolldown/plugin-babel
// astro.config.ts
import { defineConfig } from 'astro/config';

import { smallVial } from '@caspian-vega/small-vial/astro';

export default defineConfig({
    integrations: [smallVial()],
});

smallVial() installs the decorator plugin and sets vite.ssr.target to 'node'.

Outside Astro, use the vite plugin directly:

// vite.config.ts
import { smallVialDecorators } from '@caspian-vega/small-vial/vite';
import { defineConfig } from 'vite';

export default defineConfig({
    plugins: [smallVialDecorators()],
    ssr: { target: 'node' },
});

Legacy decorator versions are not supported. The plugin always applies version: '2023-11'.

Three ways to set it up

flowchart TD
  Q{What does the app need?}
  Q -->|SSR tokens on every route| A["smallVial with middleware true<br/>built-in middleware, sane defaults"]
  Q -->|Own providers, session, or middleware order| B["smallVial plus createDiMiddleware<br/>in src/middleware.ts"]
  Q -->|Routes without DI, or a non-Astro server| C["Hand rolled<br/>own tokens plus makeInjectionContext"]

1. Integration with the built-in middleware. The default, and what most apps want.

// astro.config.ts
integrations: [smallVial({ middleware: true })];

Every request gets its own container and the 7 SSR tokens. No middleware module needed, and the integration bootstraps the middleware for you: it serializes the bootstrap flags into import.meta.env.SMALL_VIAL_BOOTSTRAP, and the injected entry applies them before the first request. Pass a function when they depend on the command:

integrations: [smallVial({ middleware: true, bootstrap: ({ isDev }) => ({ debug: isDev }) })];

Global providers are found by convention on the server side. The integration looks for src/app.small-vial.config.ts, then src/config/app.small-vial.config.ts, and hands the export to createDiMiddleware({ appConfig }):

// src/app.small-vial.config.ts
import type { ApplicationConfig } from '@caspian-vega/small-vial';

export const appConfig: ApplicationConfig = [{ token: API_URL, provide: 'https://api.example.com' }];

default, appConfig and globalAppConfig are all accepted as the export name. Point the integration somewhere else with smallVial({ appConfig: 'src/core/configs/app.config.ts' }), or turn discovery off with appConfig: false.

Discovery belongs to the built-in middleware and only runs when middleware is enabled. With your own middleware module the option is inert, whatever it is set to, and the global providers are yours to pass, see setup 2 below.

The client stays manual and its location is yours to pick, ideally the first script in the head of the root layout, see Bootstrap.

2. Integration with your own middleware. Same defaults, but the middleware is yours, so it can carry global and request-scoped providers, sessions, and a position in sequence(...).

// src/middleware.ts
import { sequence } from 'astro/middleware';

import { bootstrapDi, createDiMiddleware } from '@caspian-vega/small-vial/astro';

import { globalAppConfig } from './core/configs/app.config.ts';

bootstrapDi({ appConfig: globalAppConfig });

export const onRequest = sequence(otherMiddleware, createDiMiddleware({ session: true }));

Keep middleware off in smallVial(...), otherwise both run.

With middleware off there is no app config discovery either, so smallVial({ appConfig }) is not read and does not need appConfig: false. bootstrapDi(...) in the middleware module is what seeds the global providers, and it takes the live ApplicationConfig rather than a path, so tokens stay tokens instead of going through the serialized bootstrap payload.

3. Hand rolled. Own tokens, own SSR config, own call to makeInjectionContext(...).

// src/middleware.ts
import { defineMiddleware } from 'astro/middleware';

import { type ApplicationConfig, createToken } from '@caspian-vega/small-vial';
import { makeInjectionContext } from '@caspian-vega/small-vial/server';

export const REQUEST = createToken<Request>('REQUEST');

export const onRequest = defineMiddleware(async (context, next) => {
    if (context.url.pathname.startsWith('/health')) return next();

    const ssrConfig: ApplicationConfig = [{ token: REQUEST, provide: context.request }];

    return makeInjectionContext(async () => next(), ssrConfig);
});

Recommendation: take 1, move to 2 as soon as there are global or request-scoped providers. Reach for 3 only for corner cases: excluding routes from DI, a token set that is not the Astro APIContext, or a server that is not Astro.

Details, ordering rules and the session strategy are in MIDDLEWARE_SETUP.md.

Keeping decorated class names

smallVialDecorators() also keeps the binding name of a decorated class. The 2023-11 transform otherwise drops it, so @Service() class UserService {} comes out anonymous. Injection is unaffected either way, since the container keys on the class reference, but stack traces, breakpoints and SvDebugLogger output lose the name. Turn it off with smallVialDecorators({ keepClassNames: false }).

Bootstrap

setGlobalAppConfig(config) must run before the first injection. It seeds the global providers, and a container created before it exists resolves without them.

Both sides need their own call. The middleware only covers SSR.

flowchart TD
  Q{Where does the code run?}
  Q -->|Server render| S["Discovered app.small-vial.config.ts,<br/>or module scope of the DI middleware"]
  Q -->|Browser| C["head script of the root layout<br/>runs before any island hydrates"]
  S --> I[First svInject resolves with global providers]
  C --> I

Server, discovered by the integration. Export the providers from src/app.small-vial.config.ts and nothing else is needed:

// src/app.small-vial.config.ts
export const appConfig: ApplicationConfig = [{ token: API_URL, provide: 'https://api.example.com' }];

Server, written by hand, at module scope in src/middleware/diContext.middleware.ts. It is deliberately outside the middleware handler, so it runs at import time rather than per request. bootstrapDi(...) bundles the three calls:

import { bootstrapDi, createDiMiddleware } from '@caspian-vega/small-vial/astro';

import { globalAppConfig } from '../core/configs/app.config.ts';

bootstrapDi({ appConfig: globalAppConfig, optOutDefaultContainer: true });

export const DiMiddleware = createDiMiddleware();

Browser, in the <head> of the root layout, before the islands below it hydrate:

<head>
    <script>
        import { setGlobalAppConfig } from '@caspian-vega/small-vial';
        import { globalAppConfig } from '../core/configs/app.config.ts';

        setGlobalAppConfig(globalAppConfig);
    </script>
</head>

Rules that hold on both sides:

  • Call it once, from one bootstrap location per side.
  • Call it before any svInject(...), including the ones in .astro frontmatter and in island module scope.
  • Keep the config itself in a shared module, so server and client seed the same providers.
  • On SSR the global config is not request-scoped. Request-bound providers go into the second argument of makeInjectionContext(...), and code that may run on the client must treat them as optional.

See playground/src/middleware/diContext.middleware.ts and playground/src/layouts/Layout.astro.

SSR detection

Detection reads import.meta.env.SSR, which Vite inlines per build: true in the server bundle, false in the client bundle. There is no runtime override, and none is needed on a supported host. Vitest applies the same replacement, so tests branch correctly too.

Request scoping

makeInjectionContext() gives each request its own container, so concurrent renders never share instances and request-bound values can be injected anywhere.

sequenceDiagram
  participant M as Middleware
  participant C as makeInjectionContext
  participant K as Container
  participant R as Render
  M->>C: callback, ssrConfig
  C->>K: provideContainer(config)
  K-->>C: container
  C->>R: run callback
  R->>K: svInject(token)
  K-->>R: instance
  R-->>C: Response
  C->>K: hibernate + cleanContainer
  C-->>M: Response

Astro middleware, with the tokens and the request wiring from the /astro entry:

import { bootstrapDi, createDiMiddleware } from '@caspian-vega/small-vial/astro';

import { globalAppConfig } from '../core/configs/app.config.ts';

bootstrapDi({ appConfig: globalAppConfig });

export const DiMiddleware = createDiMiddleware({
    // extra request-scoped providers, registered after the SSR tokens
    providers: ({ context }) => [{ token: REQUEST_ID, provide: context.request.headers.get('x-request-id') }],
});

The 7 SSR tokens are provided on every request:

| Token | Value | | -------------------- | -------------------- | | SSR_COOKIE | context.cookies | | SSR_REQUEST | context.request | | SSR_LOCALS | context.locals | | SSR_REQUEST_PARAMS | context.params | | SSR_URL | context.url | | SSR_SESSION | context.session | | SSR_ACTION_CONTEXT | context.callAction |

Code that also runs on the client must read them with svSecureInject(...), since they only exist during a server render.

For a session-scoped container, let the middleware resolve the session id and hand it to the strategy:

export const DiMiddleware = createDiMiddleware({
    session: true,
    contextStrategy: ({ sessionId }) =>
        sessionId ? SessionBasedContextStrategy(sessionId, { ttl: 900_000, strategy: 'stale-access' }) : undefined,
});

Writing the middleware by hand stays supported, and is what createDiMiddleware does internally:

export const DiMiddleware: MiddlewareHandler = defineMiddleware(async (context, next) => {
    const ssrConfig: ApplicationConfig = [{ token: SSR_COOKIE, provide: context.cookies }];

    return makeInjectionContext(async () => next(), ssrConfig);
});

Whatever renders the app must be called inside the callback, otherwise injection has no context to resolve from.

Testing

Vitest resolves through the same container as the app. Two things have to be set up per test file.

  • The test runner must lower the decorators. Vitest reads vite.config.ts, so smallVialDecorators() from Decorators belongs in that file's plugins, even when the app itself is wired through the smallVial() integration.
  • setOptOutDefaultContainer(true) must run before the first injection. Without it every svInject(...) outside a request context resolves from a fresh container, so instances are never shared and stubs never arrive.

With the opt out in place, injections in test mode share one container per file. Reset it between tests with teardownTestContainer().

import {
    Service,
    createToken,
    provide,
    setGlobalAppConfig,
    setOptOutDefaultContainer,
    svInject,
    teardownTestContainer,
} from '@caspian-vega/small-vial';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

const API_URL = createToken<string>('API_URL');

@Service()
class UserService {
    getUser(id: string) {
        return { id, name: 'real' };
    }
}

@Service()
class AuthService {
    private users = svInject(UserService);
    private apiUrl = svInject(API_URL);

    whoAmI(id: string) {
        return `${this.users.getUser(id).name}@${this.apiUrl}`;
    }
}

describe('AuthService', () => {
    beforeEach(() => {
        setOptOutDefaultContainer(true);
        setGlobalAppConfig([{ token: API_URL, provide: 'http://test' }]);
    });

    afterEach(() => teardownTestContainer());

    it('resolves real collaborators', () => {
        expect(svInject(AuthService).whoAmI('1')).toBe('real@http://test');
    });

    it('uses a stub when one is provided first', () => {
        provide({ token: UserService, provide: { getUser: () => ({ id: '1', name: 'stub' }) } });

        expect(svInject(AuthService).whoAmI('1')).toBe('stub@http://test');
    });
});

Stubbing rules that follow from the container:

  • Register the stub before the code under test resolves the token. The first registration wins, and an eager class is created on its first resolve.
  • provide({ token, provide }) stubs a singleton, provide({ token, factory }) stubs a value that is rebuilt on every injection.
  • setGlobalAppConfig(...) seeds providers for containers created afterwards, so call it in beforeEach, not at module scope after an injection already ran.

Request-scoped providers are tested by running the assertion inside the context:

import { makeInjectionContext } from '@caspian-vega/small-vial/server';

const REQ = createToken<{ url: string }>('REQ');

it('resolves request providers', async () => {
    const url = await makeInjectionContext(async () => svInject(REQ).url, [{ token: REQ, provide: { url: '/posts' } }]);

    expect(url).toBe('/posts');
});

See libs/small-vial/tests for the library's own specs.

Astro entry: tokens, middleware, integration

createDiMiddleware(options?)

Astro middleware that opens a request-scoped container and provides the Astro APIContext under the SSR tokens.

  • options.providers (ApplicationConfig | (ctx) => ApplicationConfig | Promise<ApplicationConfig>, optional): extra request-scoped providers, registered after the SSR tokens.
  • options.contextStrategy (ContainerContextStrategy | (ctx) => ContainerContextStrategy | undefined, optional): defaults to defaultContainerContextStrategy().
  • options.session (boolean | { key?: string; generateId?: () => string }, default false): read or create a session-bound id before the container opens, defaults key to 'sv-session' and generateId to crypto.randomUUID. The id is passed to the contextStrategy factory.
  • options.bootstrap (boolean | DiBootstrapEnvOptions, default false): call bootstrapDi(...) while the middleware is created. Merge order is defaultBootstrapOptions, the env payload written by smallVial(...), then this object.
  • options.appConfig (ApplicationConfig | (() => ApplicationConfig), optional): global providers, seeded with setGlobalAppConfig(...) while the middleware is created. The integration fills this from the discovered config module.
  • Returns: MiddlewareHandler
export const onRequest = createDiMiddleware({ session: true });

bootstrapDi(options?)

Seeds the global configuration. Runs once per side, at module scope, before the first svInject(...).

  • options.appConfig (ApplicationConfig, optional): global providers.
  • options.optOutDefaultContainer (boolean, default true): disable the client-only default container.
  • options.debug (boolean, default false): enable SvDebugLogger.
  • Returns: void

smallVial(options?)

Astro integration.

  • options.decorators (boolean | SmallVialDecoratorsOptions, default true): install the decorator plugin. The object form is forwarded to smallVialDecorators(...).
  • options.ssrTargetNode (boolean, default true): set vite.ssr.target to 'node'.
  • options.middleware (boolean | { order?: 'pre' | 'post' }, default false): add the built-in DI middleware. Leave it off when the order relative to other middleware matters, and export createDiMiddleware(...) from a middleware module instead.
  • options.appConfig (string | false, default: discovery): global providers for the server side. Only consulted when middleware is enabled, since it feeds the built-in middleware. With middleware: false nothing is discovered and the option is inert, seed the providers with bootstrapDi(...) in your own middleware module instead. Discovery order is <srcDir>/app.small-vial.config.ts, then <srcDir>/config/app.small-vial.config.ts, with .ts, .mts, .js and .mjs accepted. A string is a path relative to the project root, or an absolute one. The module exports the ApplicationConfig as default, appConfig or globalAppConfig.
  • options.bootstrap (boolean | DiBootstrapEnvOptions | (ctx) => DiBootstrapEnvOptions | false, default: true when middleware is enabled, false otherwise): bootstrap flags for the injected middleware. ctx is { command, isDev, config }. The resolved object is serialized into import.meta.env.SMALL_VIAL_BOOTSTRAP and process.env.SMALL_VIAL_BOOTSTRAP.
  • Returns: AstroIntegration
smallVial({ middleware: true, bootstrap: ({ command }) => ({ debug: command === 'dev' }) });

discoverAppConfig(srcDir)

Looks up the app config module below the Astro source directory.

  • srcDir (URL, required): Astro config.srcDir.
  • Returns: string | undefined, absolute path of the first match.

resolveAppConfigPath(appConfig, root) is the manual counterpart, it resolves a path against the project root and throws when the file is missing. APP_CONFIG_BASENAME, APP_CONFIG_LOCATIONS and APP_CONFIG_EXTENSIONS hold the convention.

readBootstrapEnv()

Reads the payload the integration wrote, from import.meta.env and then process.env.

  • Returns: DiBootstrapEnvOptions | undefined

SMALL_VIAL_BOOTSTRAP_ENV_KEY is the key it reads, defaultBootstrapOptions is { optOutDefaultContainer: true, debug: false }.

smallVialDecorators(options?)

Vite plugin that lowers 2023-11 decorators through Babel.

  • options.keepClassNames (boolean, default true): keep the binding name of a decorated class.
  • options.filter (object, default { code: '@' }): pre-filter deciding which modules reach Babel.
  • options.babelPlugins (PluginItem[], optional): extra Babel plugins, appended after the decorators plugin.
  • Returns: Promise<Plugin>, resolved by vite from the plugins array.

Exported from @caspian-vega/small-vial/vite, together with keepDecoratedClassNames for a hand-built Babel preset.

Entry points

| Import | Contains | | ------------------------------------------- | ---------------------------------------------------------------------------- | | @caspian-vega/small-vial | Decorators, injection, registration, configuration | | @caspian-vega/small-vial/server | Everything above plus makeInjectionContext and the context strategies | | @caspian-vega/small-vial/astro | SSR tokens, bootstrapDi, createDiMiddleware, the smallVial integration | | @caspian-vega/small-vial/astro-middleware | onRequest, the ready-made middleware for addMiddleware(...) | | @caspian-vega/small-vial/vite | smallVialDecorators, the build-time decorator plugin |

/astro needs astro, /vite needs @babel/core, @babel/plugin-proposal-decorators and @rolldown/plugin-babel. All four are optional peer dependencies, installed only by the entries that use them.

API

Service(behaviour?), Store, Injectable, Controller, Util

Class decorators that register the decorated class. They behave identically and differ only in the archetype they name.

  • behaviour ('EAGER' | 'LAZY', default 'EAGER'): 'LAZY' registers the class without instantiating it until provide(...) is called.
  • Returns: a class decorator.
@Service()
class Mailer {}

@Store('LAZY')
class DraftStore {}

InjectableTSEX(behaviour?)

Same as Injectable, for the legacy TypeScript experimental decorator pipeline.

  • behaviour ('EAGER' | 'LAZY', default 'EAGER')
  • Returns: a class decorator.

svInject(token)

Resolves from the current injection context, global or request-scoped.

  • token (Tokenizable<T> | Class<T>, required)
  • Returns: T
  • Throws: Error when nothing is registered for the token.
const auth = svInject(AuthService);

svInjectOptional(token)

  • token (Tokenizable<T>, required)
  • Returns: T | undefined
const value = svInjectOptional(SSR_ONLY_TOKEN);

svSecureInject(token)

Returns a resolver for a token that may be absent, so one implementation can read from SSR on the server and fall back to the browser on the client.

  • token (Tokenizable<T>, required)
  • Returns: SafeInjectFn<T>. Call it with a default value, or call secureHandle(fn) to receive the resolved value or undefined and branch yourself.
@Store()
export class AppMetaStore {
    private SSRCookie = svSecureInject<AstroCookies>(SSR_COOKIE);

    public readonly isDark = atom(
        this.SSRCookie.secureHandle((value) =>
            value ? value.get('theme')?.value === 'dark' : this.loadThemeFromCSRCookie() === 'dark'
        )
    );
}

provide(provision)

Registers a provider, factory, or lazy class in the current container.

  • provision (Tokenizable<T> | Provider | Class<T>, required)
  • Returns: void
provide({ token: LOGGER, factory: () => new ConsoleLogger() });
provide(LazyService);

svEject(token, force?)

Removes an instance from the container and runs its onEject().

  • token (Tokenizable | Class<T>, required)
  • force (boolean, default false): remove even when other holders remain.
  • Returns: boolean, true when something was removed.
onDestroy(() => svEject(LazyService));

createToken(id)

Creates a typed token.

  • id (string, required): unique, non-empty. Duplicate ids collide silently.
  • Returns: Tokenizable<T>
const API_URL = createToken<string>('API_URL');

initContainer()

Returns the current container, creating it when needed.

  • Returns: Container

teardownTestContainer()

Drops the test container.

  • Returns: void
afterEach(() => teardownTestContainer());

setGlobalAppConfig(config)

Sets global and client-side providers. Not request-scoped on SSR.

  • config (ApplicationConfig, required)
  • Returns: void
  • Must run before any container is initialized, that is before the first svInject(...). Call it once per side: at module scope in the DI middleware on the server, and in a <head> script of the root layout in the browser. See Bootstrap.
setGlobalAppConfig([{ token: API_URL, provide: 'https://api.example.com' }]);

appConfig()

  • Returns: ApplicationConfig, the currently registered global config.

setOptOutDefaultContainer(value)

Disables the client-only default container. Required for SSR request scoping.

  • value (boolean, required)
  • Returns: void

getSSRStorage()

  • Returns: the AsyncLocalStorage instance holding request containers, or undefined before the server entry is imported.

CONTAINER_KEY

string, key under which the request container is stored.

makeInjectionContext(callback, config?, contextStrategy?)

Server entry only. Runs callback inside a request-scoped container.

  • callback (() => Promise<T>, required): must contain whatever renders the app.
  • config (ApplicationConfig, optional): request-scoped providers.
  • contextStrategy (ContainerContextStrategy, optional): defaults to defaultContainerContextStrategy().
  • Returns: Promise<T>, the callback's result.

Teardown runs whether the callback resolves or throws.

return makeInjectionContext(async () => next(), ssrConfig);

defaultContainerContextStrategy()

  • Returns: ContainerContextStrategy that creates a fresh container per request, calls postConstruct(), and clears it with CLEAN_ALL() afterwards.

SessionBasedContextStrategy(sessionId, sessionPolicy)

Keeps containers in memory and reuses them per session id.

  • sessionId (string, required): use the Astro session id in Astro setups.
  • sessionPolicy.ttl (number, required): lifetime in milliseconds.
  • sessionPolicy.strategy ('stale-access' | 'since-created', required): 'stale-access' refreshes the TTL on each access, 'since-created' does not.
  • Returns: ContainerContextStrategy
return makeInjectionContext(
    async () => next(),
    ssrConfig,
    SessionBasedContextStrategy(sessionId, {
        ttl: 900_000,
        strategy: 'stale-access',
    })
);

Constraints of this strategy:

  • The whole container graph stays in memory per active session.
  • Lifecycle hooks become load-bearing, since onHibernate and onResume run between requests.
  • Sockets, polling, intervals and timers must clean up explicitly or they leak.
  • Session containers are never serialized.
  • Behind a proxy or load balancer, session affinity on the session id cookie must be enabled.

SvDebugLogger

Namespace export.

import { SvDebugLogger } from '@caspian-vega/small-vial';

SvDebugLogger.default.enable();

Lifecycle hooks

| Interface | Method | Runs when | | ------------------- | ----------------- | -------------------------------------------------------------------- | | PostConstructable | postConstruct() | The container is ready and the instance has been created | | Ejectable | onEject() | svEject(...) removes the instance, or the container is cleared | | Hibernateable | onHibernate() | A request completes and the container is kept. Session strategy only | | Resumable | onResume() | A kept container is reused for a new request. Session strategy only |

@Service()
class LiveFeedService implements PostConstructable, Ejectable, Hibernateable, Resumable {
    postConstruct() {}
    onHibernate() {}
    onResume() {}
    onEject() {}
}

Types

Class

(new (...args: any[]) => T) carrying optional _service_prop, _service_tag, _service_lazy.

Tokenizable

  • prototype (Class<T>, optional)
  • id (string, optional)

Provider

  • token (Tokenizable<T> | Class<T>, required)
  • provide (T, optional): instance or value, a singleton within the container.
  • factory (() => T, optional): runs on every injection.
const config: ApplicationConfig = [
    { token: LOGGER, factory: () => (isDevMode() ? new ConsoleLogger() : new RemoteLogger()) },
];

ApplicationConfig

Provider[]

DiBootstrapEnvOptions

BootstrapDiOptions without appConfig, that is the serializable half.

  • optOutDefaultContainer (boolean, optional)
  • debug (boolean, optional)

SafeInjectFn

  • Call signature (defaultValue: T) => T
  • secureHandle<S>(handler: (value: T | undefined) => S): S

SafeInjector

  • get(): T
  • getOrElse(defaultValue: T): T

ContainerContextStrategy

  • provideContainer(config?: ApplicationConfig): Container
  • cleanContainer?(container: Container): void

Container

  • postConstruct(): void
  • registerProvider(token, instance): void
  • registerFactory(token, factory): void
  • loadConfig(config, overwrite?): void
  • eject(token, force?): boolean
  • CLEAN_ALL(): void
  • hibernate(): void
  • resume(): void

Constraints

  • Registered classes are singletons within their container. Use a factory provider for non-singleton instances.
  • Factories run on every injection. Use provide with a pre-built instance for singleton behaviour, or memoize inside the factory and manage that cache.
  • Registration is eager by default. 'LAZY' registers without instantiating, and the instance appears only after an explicit provide(...).
  • Circular dependencies are not resolved. Move the injection into postConstruct, into the method that needs it, or use svSecureInject.
  • Injecting at provider-creation time does not work. Use postConstruct or inject on demand.
  • There is no island or module scope. Add containers to the root injection context yourself, or use factories for narrower scopes.
  • Requires SSR mode, at least standalone, and a promise-based render cycle.

Synergies

See SYNERGIES.md.

License

MIT