diff --git a/packages/state/package.json b/packages/state/package.json index 41c7e8a..8d2fb77 100644 --- a/packages/state/package.json +++ b/packages/state/package.json @@ -102,9 +102,9 @@ "vitest": "^0.26.3" }, "dependencies": { + "@solid-primitives/event-bus": "^1.0.8", "rxjs": "^7.8.0", - "solid-js": "^1.7.0", - "@solid-primitives/event-bus": "^1.0.8" + "solid-js": "^1.7.0" }, "peerDependenciesMeta": { "@solid-primitives/event-bus": { diff --git a/packages/state/src/api.ts b/packages/state/src/api.ts index f1aca3f..583ef82 100644 --- a/packages/state/src/api.ts +++ b/packages/state/src/api.ts @@ -1,7 +1,6 @@ import { ApiDefinitionCreator, GenericStoreApi, - HookConsumerFunction, Plugin, PluginContext, PluginOf, @@ -9,6 +8,8 @@ import { } from '~/types'; import { onCleanup } from 'solid-js'; import { ApiDefinition } from '~/apiDefinition'; +import { Container } from '~/container'; +import { ResolvedPluginContext } from '~/resolved-plugin-context'; export const $CREATOR = Symbol('store-creator-api'), $PLUGIN = Symbol('store-plugin'); @@ -56,26 +57,18 @@ function checkDependencies( * * @template TDefinition - The type of the store API definition. * @param definition - The store API definition to resolve. + * @param container - The StateContainer in the plugin context. * @returns The resolved store API with all the extensions applied. */ export function resolve< TDefinition extends StoreApiDefinition>, ->(definition: TDefinition) { +>(definition: TDefinition, container?: Container) { const api = definition[$CREATOR]; const { factory, plugins } = api; - const initSubscriptions = new Set(), - destroySubscriptions = new Set(), - resolvedPlugins: string[] = [], - pluginContext: PluginContext = { - plugins, - hooks: { - onInit: (callback) => initSubscriptions.add(callback), - onDestroy: (callback) => destroySubscriptions.add(callback), - }, - metadata: new Map(), - }, + const resolvedPlugins: string[] = [], + pluginContext = new ResolvedPluginContext(container, plugins), resolvedStore = factory(); for (const extensionCreator of plugins) { @@ -99,17 +92,8 @@ export function resolve< resolvedPlugins.push(extensionCreator.name); } - for (const listener of initSubscriptions) { - listener(resolvedStore); - initSubscriptions.delete(listener); - } - - onCleanup(() => { - for (const listener of destroySubscriptions) { - listener(resolvedStore); - destroySubscriptions.delete(listener); - } - }); + pluginContext.runInitSubscriptions(resolvedStore); + onCleanup(() => pluginContext.runDestroySubscriptions(resolvedStore)); return resolvedStore; } @@ -131,7 +115,7 @@ type PluginCreatorOptions = { function _makePlugin< TCallback extends ( store: S, - context: PluginContext, + context: PluginContext, ) => unknown, >( pluginCallback: TCallback, diff --git a/packages/state/src/apiDefinition.ts b/packages/state/src/apiDefinition.ts index 6c598b4..ca8376d 100644 --- a/packages/state/src/apiDefinition.ts +++ b/packages/state/src/apiDefinition.ts @@ -30,7 +30,7 @@ export class ApiDefinition } extend( - createPlugin: (ctx: T & E, context: PluginContext) => TExtendedSignal, + createPlugin: (ctx: T & E, context: PluginContext) => TExtendedSignal, ): ApiDefinitionCreator> { if ( typeof createPlugin === 'function' && diff --git a/packages/state/src/container.ts b/packages/state/src/container.ts index 3c551fe..99c1dd4 100644 --- a/packages/state/src/container.ts +++ b/packages/state/src/container.ts @@ -1,4 +1,4 @@ -import { getOwner, Owner, runWithOwner } from 'solid-js'; +import { getOwner, type Owner, runWithOwner } from 'solid-js'; import { ExtractStore, GenericStoreApi, StoreApiDefinition } from './types'; import { $CREATOR, resolve } from './api'; @@ -48,7 +48,7 @@ export class Container { const resolvedOwner = this.#resolveOwner(state, owner); const store = runWithOwner(resolvedOwner, () => { try { - return resolve(state); + return resolve(state, this); } catch (e) { error = e as Error; } diff --git a/packages/state/src/resolved-plugin-context.ts b/packages/state/src/resolved-plugin-context.ts new file mode 100644 index 0000000..c1abab0 --- /dev/null +++ b/packages/state/src/resolved-plugin-context.ts @@ -0,0 +1,49 @@ +import { + ExtractStore, + GenericStoreApi, + HookConsumerFunction, + Plugin, + PluginContext, + PluginHooks, + StoreApiDefinition, +} from '~/types'; +import { Container } from '~/container'; + +export class ResolvedPluginContext implements PluginContext { + private readonly initSubscriptions = new Set(); + private readonly destroySubscriptions = new Set(); + public readonly metadata: Map = new Map(); + + hooks: PluginHooks = { + onInit: (callback) => this.initSubscriptions.add(callback), + onDestroy: (callback) => this.destroySubscriptions.add(callback), + }; + + constructor( + private readonly container: Container | undefined, + public readonly plugins: Plugin[], + ) {} + + inject>( + storeDefinition: TStoreDefinition, + ): ExtractStore { + if (!this.container) { + throw new Error('[statebuilder] No container set in current context.'); + } + return this.container.get(storeDefinition); + } + + public runInitSubscriptions(resolvedStore: GenericStoreApi) { + for (const listener of this.initSubscriptions) { + listener(resolvedStore); + this.initSubscriptions.delete(listener); + } + } + + public runDestroySubscriptions(resolvedStore: GenericStoreApi) { + for (const listener of this.destroySubscriptions) { + listener(resolvedStore); + this.destroySubscriptions.delete(listener); + } + } +} diff --git a/packages/state/src/types.ts b/packages/state/src/types.ts index 0e7d25b..bb98f64 100644 --- a/packages/state/src/types.ts +++ b/packages/state/src/types.ts @@ -18,7 +18,7 @@ export interface ApiDefinitionCreator< extend( createPlugin: ( ctx: TStoreApi & TSignalExtension, - context: PluginContext, + context: PluginContext, ) => TExtendedSignal, ): ApiDefinitionCreator< TStoreApi, @@ -84,7 +84,7 @@ export type GetStoreApiState = export type HookConsumerFunction = (api: T) => void; -interface PluginHooks { +export interface PluginHooks { onInit: (consumer: HookConsumerFunction) => void; onDestroy: (consumer: HookConsumerFunction) => void; @@ -94,6 +94,10 @@ export type PluginContext = { plugins: readonly Plugin[]; metadata: Map; hooks: PluginHooks; + + inject>( + storeDefinition: TStoreDefinition, + ): ExtractStore; }; export type PluginCreatorFunction< diff --git a/packages/state/test/api.test.ts b/packages/state/test/api.test.ts index b3ebb3e..45c7988 100644 --- a/packages/state/test/api.test.ts +++ b/packages/state/test/api.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import { $CREATOR, $PLUGIN, create, makePlugin, resolve } from '../src/api'; -import { createRoot, createSignal } from 'solid-js'; +import { createRoot, createSignal, getOwner } from 'solid-js'; import { Container } from '../src/container'; import { GenericStoreApi } from '~/types'; import { SetStoreFunction } from 'solid-js/store'; @@ -167,3 +167,34 @@ describe('resolve', () => { }); }); }); + +describe('inject', () => { + const initHook = vi.fn(); + const destroyHook = vi.fn(); + + it('should inject state inside another state', () => { + createRoot((dispose) => { + const containerState = Container.create(getOwner()!); + + const State1 = defineSignal(() => 0).extend((_, context) => { + context.hooks.onInit(() => initHook('first-plugin')); + context.hooks.onDestroy(() => destroyHook('first-plugin')); + }); + + const State2 = defineSignal(() => 1).extend((_, context) => { + const state1 = context.inject(State1); + return { state1 }; + }); + + const state2 = containerState.get(State2); + const state1 = containerState.get(State1); + + expect(initHook).toHaveBeenCalledTimes(1); + expect(state2.state1).toEqual(state1); + + dispose(); + + expect(destroyHook).toHaveBeenCalledTimes(1); + }); + }); +});