Skip to content

Commit

Permalink
feat: allow to inject states inside builders
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardoperra committed Oct 22, 2023
1 parent f9343c8 commit 0b35308
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 33 deletions.
4 changes: 2 additions & 2 deletions packages/state/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
34 changes: 9 additions & 25 deletions packages/state/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {
ApiDefinitionCreator,
GenericStoreApi,
HookConsumerFunction,
Plugin,
PluginContext,
PluginOf,
StoreApiDefinition,
} 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');
Expand Down Expand Up @@ -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<GenericStoreApi, Record<string, any>>,
>(definition: TDefinition) {
>(definition: TDefinition, container?: Container) {
const api = definition[$CREATOR];

const { factory, plugins } = api;

const initSubscriptions = new Set<HookConsumerFunction>(),
destroySubscriptions = new Set<HookConsumerFunction>(),
resolvedPlugins: string[] = [],
pluginContext: PluginContext = {
plugins,
hooks: {
onInit: (callback) => initSubscriptions.add(callback),
onDestroy: (callback) => destroySubscriptions.add(callback),
},
metadata: new Map<string, unknown>(),
},
const resolvedPlugins: string[] = [],
pluginContext = new ResolvedPluginContext(container, plugins),
resolvedStore = factory();

for (const extensionCreator of plugins) {
Expand All @@ -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;
}
Expand All @@ -131,7 +115,7 @@ type PluginCreatorOptions = {
function _makePlugin<
TCallback extends <S extends GenericStoreApi>(
store: S,
context: PluginContext,
context: PluginContext<S>,
) => unknown,
>(
pluginCallback: TCallback,
Expand Down
2 changes: 1 addition & 1 deletion packages/state/src/apiDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class ApiDefinition<T extends GenericStoreApi, E extends {}>
}

extend<TExtendedSignal extends {} | void>(
createPlugin: (ctx: T & E, context: PluginContext) => TExtendedSignal,
createPlugin: (ctx: T & E, context: PluginContext<T>) => TExtendedSignal,
): ApiDefinitionCreator<T, TExtendedSignal & Omit<E, keyof TExtendedSignal>> {
if (
typeof createPlugin === 'function' &&
Expand Down
4 changes: 2 additions & 2 deletions packages/state/src/container.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
}
Expand Down
49 changes: 49 additions & 0 deletions packages/state/src/resolved-plugin-context.ts
Original file line number Diff line number Diff line change
@@ -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<HookConsumerFunction>();
private readonly destroySubscriptions = new Set<HookConsumerFunction>();
public readonly metadata: Map<string, unknown> = new Map<string, unknown>();

hooks: PluginHooks<GenericStoreApi> = {
onInit: (callback) => this.initSubscriptions.add(callback),
onDestroy: (callback) => this.destroySubscriptions.add(callback),
};

constructor(
private readonly container: Container | undefined,
public readonly plugins: Plugin<any, any>[],
) {}

inject<TStoreDefinition extends StoreApiDefinition<any, any>>(
storeDefinition: TStoreDefinition,
): ExtractStore<TStoreDefinition> {
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);
}
}
}
8 changes: 6 additions & 2 deletions packages/state/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ApiDefinitionCreator<
extend<TExtendedSignal extends {} | void>(
createPlugin: (
ctx: TStoreApi & TSignalExtension,
context: PluginContext,
context: PluginContext<TStoreApi>,
) => TExtendedSignal,
): ApiDefinitionCreator<
TStoreApi,
Expand Down Expand Up @@ -84,7 +84,7 @@ export type GetStoreApiState<T extends GenericStoreApi> =
export type HookConsumerFunction<T extends GenericStoreApi = GenericStoreApi> =
(api: T) => void;

interface PluginHooks<T extends GenericStoreApi> {
export interface PluginHooks<T extends GenericStoreApi> {
onInit: (consumer: HookConsumerFunction<T>) => void;

onDestroy: (consumer: HookConsumerFunction<T>) => void;
Expand All @@ -94,6 +94,10 @@ export type PluginContext<T extends GenericStoreApi = GenericStoreApi> = {
plugins: readonly Plugin<any, any>[];
metadata: Map<string, unknown>;
hooks: PluginHooks<T>;

inject<TStoreDefinition extends StoreApiDefinition<any, any>>(
storeDefinition: TStoreDefinition,
): ExtractStore<TStoreDefinition>;
};

export type PluginCreatorFunction<
Expand Down
33 changes: 32 additions & 1 deletion packages/state/test/api.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
});
});
});

0 comments on commit 0b35308

Please sign in to comment.