Skip to content

Commit

Permalink
Local owner context (#31)
Browse files Browse the repository at this point in the history
* feat: local owner context
* docs(changeset): local owner context
* changeset: add local owner context
* allow to inject states from parent context
* docs(changeset): allows to inject states from parent context
* changeset: snapshot
* improve names
* improve names
* add StateBuilder error
* rework state from parent container, add test
* better error handling
* docs(changeset): Allows to resolve plugin in context
  • Loading branch information
riccardoperra authored Nov 8, 2023
1 parent 53052d9 commit 3ce5a15
Show file tree
Hide file tree
Showing 19 changed files with 262 additions and 117 deletions.
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

0 comments on commit 3ce5a15

Please sign in to comment.