Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local owner context #31

Merged
merged 12 commits into from
Nov 8, 2023
9 changes: 9 additions & 0 deletions .changeset/metal-teachers-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'statebuilder': minor
---

- States will now get resolved using their Container reactive context instead of the context where they are defined.
- Errors thrown through the state lifecycle will return a `StateBuilderError` class.
- Add the `remove` method to the Container API to destroy a state
- Add container hierarchy
- Export `ɵdefineResource` and`ɵWithResourceStorage` state resource API
14 changes: 14 additions & 0 deletions examples/counter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# counter

## 0.0.0-20231028092513

### Patch Changes

- Updated dependencies [4b03fb8]
- statebuilder@0.0.0-20231028092513

## 0.0.0-20231027182437

### Patch Changes

- Updated dependencies [cc23c8e]
- statebuilder@0.0.0-20231027182437

## 0.0.13

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion examples/counter/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "counter",
"version": "0.0.13",
"version": "0.0.0-20231028092513",
"scripts": {
"dev": "solid-start dev",
"build": "solid-start build",
Expand Down
49 changes: 40 additions & 9 deletions examples/counter/src/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import './Counter.css';
import { defineStore, provideState } from 'statebuilder';
import {
defineSignal,
defineStore,
provideState,
StateProvider,
} from 'statebuilder';
import { withProxyCommands } from 'statebuilder/commands';
import { withReduxDevtools } from 'statebuilder/devtools';
import { withAsyncAction } from 'statebuilder/asyncAction';
Expand All @@ -26,7 +31,12 @@ function appReducer(state: AppState, action: AppActions) {
}
}

const $store = defineStore(() => ({
const GlobalCount = defineSignal(() => 1).extend((_, context) => {
context.hooks.onInit(() => console.log('init count2'));
context.hooks.onDestroy(() => console.log('destroy count2'));
});

const CountStore = defineStore(() => ({
count: 0,
}))
.extend(withAsyncAction())
Expand All @@ -37,24 +47,36 @@ const $store = defineStore(() => ({
)
.extend(withReduxDevtools({ storeName: 'countStore' }))
.extend(withReducer(appReducer))
.extend((ctx) => {
ctx.hold(ctx.commands.increment, () =>
ctx.set('count', (count) => count + 1),
.extend((state, context) => {
state.hold(state.commands.increment, () =>
state.set('count', (count) => count + 1),
);

context.hooks.onInit(() => console.log('init'));
context.hooks.onDestroy(() => console.log('destroy'));

return {
incrementAfter1S: ctx.asyncAction(async (payload: number) => {
incrementAfter1S: state.asyncAction(async (payload: number) => {
await new Promise((r) => setTimeout(r, 3000));
ctx.set('count', (count) => count + 1);
state.set('count', (count) => count + 1);
return payload;
}),
};
});

export default function Counter() {
const store = provideState($store);
function Counter() {
const store = provideState(CountStore);
const globalCount = provideState(GlobalCount);

return (
<>
<button
class="increment"
onClick={() => globalCount.set((count) => count + 1)}
>
Global Count Clicks: {globalCount()}
</button>

<button
class="increment"
onClick={() => store.actions.increment()}
Expand All @@ -79,3 +101,12 @@ export default function Counter() {
</>
);
}

export default function CounterRoot() {
const globalCount = provideState(GlobalCount);
return (
<StateProvider>
<Counter />
</StateProvider>
);
}
12 changes: 12 additions & 0 deletions packages/state/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# plustate

## 0.0.0-20231028092513

### Patch Changes

- 4b03fb8: allows to inject states from parent context

## 0.0.0-20231027182437

### Patch Changes

- cc23c8e: local owner context

## 0.5.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/state/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"type": "git",
"url": "https://github.com/riccardoperra/statebuilder"
},
"version": "0.5.0",
"version": "0.0.0-20231028092513",
"source": "src/index.ts",
"module": "dist/index.js",
"main": "dist/index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Plugin,
PluginContext,
} from '~/types';
import { getOwner } from 'solid-js';
import { $CREATOR, $PLUGIN } from '~/api';

/**
Expand All @@ -14,19 +13,14 @@ import { $CREATOR, $PLUGIN } from '~/api';
export class ApiDefinition<T extends GenericStoreApi, E extends {}>
implements ApiDefinitionCreator<T, E>
{
[$CREATOR]: ApiDefinitionInternalCreator<T, E>;
readonly [$CREATOR]: ApiDefinitionInternalCreator<T, E>;
#customPluginId: number = 0;
#plugins: Array<Plugin<any, any>> = [];
readonly #id: number = 0;
readonly #plugins: Array<Plugin<any, any>> = [];

constructor(name: string, id: number, factory: () => T) {
const owner = getOwner();

this[$CREATOR] = {
name,
plugins: this.#plugins,
owner,
factory,
};
this[$CREATOR] = { name, plugins: this.#plugins, factory };
this.#id = id;
}

extend<TExtendedSignal extends {} | void>(
Expand Down
30 changes: 20 additions & 10 deletions packages/state/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
StoreApiDefinition,
} from '~/types';
import { onCleanup } from 'solid-js';
import { ApiDefinition } from '~/apiDefinition';
import { ApiDefinition } from '~/api-definition';
import { Container } from '~/container';
import { ResolvedPluginContext } from '~/resolved-plugin-context';
import { StateBuilderError } from '~/error';

export const $CREATOR = Symbol('store-creator-api'),
$PLUGIN = Symbol('store-plugin');

/**
* A factory function that creates a store API definition creator.
*
Expand Down Expand Up @@ -44,8 +44,8 @@ function checkDependencies(

dependencies.forEach((dependency) => {
if (!resolvedPlugins.includes(dependency)) {
throw new Error(
`[statebuilder] The dependency '${dependency}' of plugin '${meta.name}' is missing`,
throw new StateBuilderError(
`The dependency '${dependency}' of plugin '${meta.name}' is missing`,
{ cause: { resolvedDependencies: resolvedPlugins, plugin } },
);
}
Expand All @@ -63,10 +63,7 @@ function checkDependencies(
export function resolve<
TDefinition extends StoreApiDefinition<GenericStoreApi, Record<string, any>>,
>(definition: TDefinition, container?: Container) {
const api = definition[$CREATOR];

const { factory, plugins } = api;

const { factory, plugins } = definition[$CREATOR];
const resolvedPlugins: string[] = [],
pluginContext = new ResolvedPluginContext(container, plugins),
resolvedStore = factory();
Expand All @@ -92,8 +89,21 @@ export function resolve<
resolvedPlugins.push(extensionCreator.name);
}

pluginContext.runInitSubscriptions(resolvedStore);
onCleanup(() => pluginContext.runDestroySubscriptions(resolvedStore));
if (!!container) {
pluginContext.hooks.onDestroy(() => container.remove(definition));
}

for (const listener of pluginContext.initSubscriptions) {
listener(resolvedStore);
pluginContext.initSubscriptions.delete(listener);
}

onCleanup(() => {
for (const listener of pluginContext.destroySubscriptions) {
listener(resolvedStore);
pluginContext.destroySubscriptions.delete(listener);
}
});

return resolvedStore;
}
Expand Down
67 changes: 40 additions & 27 deletions packages/state/src/container.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
import { getOwner, type Owner, runWithOwner } from 'solid-js';
import { getOwner, type Owner } from 'solid-js';
import { ExtractStore, GenericStoreApi, StoreApiDefinition } from './types';
import { $CREATOR, resolve } from './api';
import { runInSubRoot } from '~/root';
import { StateBuilderError } from '~/error';

export class Container {
private readonly states = new Map<string, GenericStoreApi>();

protected constructor(private readonly owner: Owner) {}
protected constructor(
private readonly owner: Owner,
private readonly parent?: Container,
) {}

static create(owner?: Owner) {
static create(owner?: Owner, parentContainer?: Container) {
const resolvedOwner = owner ?? getOwner()!;
if (!resolvedOwner) {
console.warn(
'[statebuilder] Using StateContainer without <StateProvider/> or `createRoot()` context is discouraged',
);
}
return new Container(resolvedOwner);
return new Container(resolvedOwner, parentContainer);
}

remove<TStoreDefinition extends StoreApiDefinition<any, any>>(
state: TStoreDefinition,
): void {
this.states.delete(state[$CREATOR].name);
}

get<TStoreDefinition extends StoreApiDefinition<any, any>>(
state: TStoreDefinition,
): ExtractStore<TStoreDefinition> {
type TypedStore = ExtractStore<TStoreDefinition>;

if (!state[$CREATOR]) {
throw new StateBuilderError('No state $CREATOR found.', {
cause: { state: state },
});
}
try {
const name = state[$CREATOR].name;
const instance = this.states.get(name);
const instance = this.#retrieveInstance(name);
if (instance) {
return instance as unknown as TypedStore;
return instance as TypedStore;
}
const store = this.#resolveStore(this.owner, state);
this.states.set(name, store!);
return store as TypedStore;
} catch (exception) {
if (exception instanceof Error) throw exception;
throw new Error(
'[statebuilder] An error occurred during store initialization',
throw new StateBuilderError(
'An error occurred during store initialization',
{ cause: exception },
);
}
Expand All @@ -43,25 +58,23 @@ export class Container {
#resolveStore<TStoreDefinition extends StoreApiDefinition<any, any>>(
owner: Owner,
state: TStoreDefinition,
) {
let error: Error | undefined;
const resolvedOwner = this.#resolveOwner(state, owner);
const store = runWithOwner(resolvedOwner, () => {
try {
return resolve(state, this);
} catch (e) {
error = e as Error;
}
});
if (error) throw error;
return store;
): GenericStoreApi {
return runInSubRoot(() => resolve(state, this), owner);
}

#resolveOwner<TStoreDefinition extends StoreApiDefinition<any, any>>(
state: TStoreDefinition,
fallbackOwner: Owner,
) {
const metadata = state[$CREATOR];
return metadata.owner ?? fallbackOwner;
#retrieveInstance(name: string): GenericStoreApi | null {
let instance: GenericStoreApi | null = this.states.get(name) ?? null;
if (instance) {
return instance;
}
let currentParent = this.parent;
while (currentParent) {
instance = currentParent.states.get(name) ?? null;
if (!!instance) {
return instance;
}
currentParent = currentParent.parent;
}
return instance;
}
}
5 changes: 5 additions & 0 deletions packages/state/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class StateBuilderError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(`[statebuilder] ${message}`, options);
}
}
5 changes: 4 additions & 1 deletion packages/state/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ export { Container } from './container';
export {
StateProvider,
provideState,
getStateContext,
defineSignal,
defineStore,
ɵdefineResource,
ɵWithResourceStorage,
} from './solid';

export type { Signal, Store } from './solid';
export type { Signal, Store, Resource } from './solid';

export { create, makePlugin } from './api';

Expand Down
Loading
Loading