Skip to content

Commit

Permalink
Handle exceptions in molecules (#59)
Browse files Browse the repository at this point in the history
## Description of the change

Updates the internals of `injector` to make it gracefully handle broken
molecules, instead of leaving `use` in a broken state. Fixes #61

## Type of change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Documentation or Development tools (readme, specs, tests, code
formatting)
  • Loading branch information
loganvolkers authored May 27, 2024
1 parent 38f2a66 commit 58ccacf
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-spies-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bunshi": patch
---

Improve handling for molecules that throw exceptions. This will no longer break the global `use` functions, or break `getMol` or `getScope`. Fixes #61
4 changes: 4 additions & 0 deletions examples/old-typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/react/ScopeProvider.reactivity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const TestCope = createScope<{ number?: number }>({});
const lifecycle = createLifecycleUtils();
const TestMol = molecule(() => {
const scope = use(TestCope);
console.log(scope.number); //In the first case, there is no change, and in the second case, the value from one timing ago is output.
// console.log(scope.number); //In the first case, there is no change, and in the second case, the value from one timing ago is output.
lifecycle.connect(scope.number);
return { scope };
});
Expand Down
48 changes: 25 additions & 23 deletions src/vanilla/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,29 +647,31 @@ function runMolecule(
useImpl.push(use);
let running = true;
trackingScopeGetter(InternalOnlyGlobalScope);
const value = m[GetterSymbol](trackingGetter, trackingScopeGetter);
running = false;
onMountImpl.pop();
useImpl.pop();

return {
deps: {
molecules: dependentMolecules,
allScopes,
defaultScopes,
mountedCallbacks,
/**
* Returns a copy
*
* Reverses the order so that the deepest dependencies are at the top
* of the list. This will be important for ensuring ordering for how
* mounts are called with transient dependencies.
*
*/
buddies: buddies.slice().reverse(),
},
value,
};
try {
const value = m[GetterSymbol](trackingGetter, trackingScopeGetter);
return {
deps: {
molecules: dependentMolecules,
allScopes,
defaultScopes,
mountedCallbacks,
/**
* Returns a copy
*
* Reverses the order so that the deepest dependencies are at the top
* of the list. This will be important for ensuring ordering for how
* mounts are called with transient dependencies.
*
*/
buddies: buddies.slice().reverse(),
},
value,
};
} finally {
running = false;
onMountImpl.pop();
useImpl.pop();
}
}

type CreationProps = {
Expand Down
23 changes: 23 additions & 0 deletions src/vanilla/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { AnyScopeTuple } from "./internal/internal-types";
import { onMount, onUnmount, use } from "./lifecycle";
import { molecule } from "./molecule";
import { createScope } from "./scope";
import {
ConfigMolecule,
ConfigScope,
LibaryMolecule,
} from "./testing/test-molecules";

const defaultFn = vi.fn();
const ExampleScope = createScope<Function>(defaultFn);
Expand Down Expand Up @@ -444,6 +449,24 @@ describe("Conditional dependencies", () => {
expect(() => injector.use(TwoForks)).toThrow();
});
});

describe("Required scope is a molecule", () => {
test("Use the molecule, expect error", () => {
const injector = createInjector();
expect(() => injector.use(LibaryMolecule)).toThrow();
});
test("Non-conditional path works", () => {
const injector = createInjector();
const [library, unsub1] = injector.use(LibaryMolecule, [
ConfigScope,
ConfigMolecule,
]);
const [config, unsub2] = injector.use(ConfigMolecule);
expect(library.example).toBe(config.example);
unsub1();
unsub2();
});
});
});

describe("unmount lifecycle", () => {
Expand Down
29 changes: 28 additions & 1 deletion src/vanilla/testing/test-molecules.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { atom, PrimitiveAtom } from "jotai";
import { Atom, atom, PrimitiveAtom } from "jotai";
import { molecule } from "../molecule";
import { createScope } from "../scope";
import { ScopeTuple } from "../types";
import { Molecule, use } from "..";

type BaseAtoms = {
nameAtom: PrimitiveAtom<string>;
Expand Down Expand Up @@ -49,3 +50,29 @@ export const companyMolecule = molecule((_, getScope) => {
companyNameAtom,
};
});

type Config = {
example: Atom<string>;
};

export const ConfigMolecule = molecule<Config>(() => {
return {
example: atom("example"),
};
});

export const ConfigScope = createScope<Molecule<Config> | undefined>(undefined);

export const LibaryMolecule = molecule(() => {
const configMol = use(ConfigScope);
if (!configMol)
throw new Error("This molecule requires ConfigScope to function!");

const config = use(configMol) as Config;
const derivedAtom = atom((get) => get(config.example).toUpperCase());

return {
...config,
derivedAtom,
};
});

0 comments on commit 58ccacf

Please sign in to comment.