Skip to content

Commit

Permalink
feat: the LifecycleResource has its first passing test
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed Jun 6, 2021
1 parent 82f258a commit cef4396
Show file tree
Hide file tree
Showing 8 changed files with 665 additions and 95 deletions.
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,19 @@ ember install ember-resources

```ts
class MyClass {
data = useResource(SomeResource, () => [arg list]);
data = useResource(this, SomeResource, () => [arg list]);
}
```

When any tracked data in the args thunk, the `update` function on `SomeResource`
will be called.

the args thunk accepts the following data shapes:
The `this` is to keep track of destruction -- so when `MyClass` is destroyed, all the resources attached to it can also be destroyed.

The args thunk accepts the following data shapes:
```
() => [an, array]
() => ({ named: 'args', hello: 'there' })
() => ({ hello: 'there' })
() => ({ named: {...}, positional: [...] })
```
#### An array
Expand All @@ -52,13 +54,13 @@ and `this.args.positional` will contain the result of the thunk.

#### An object of named args

when an object is passed where the key `named` is not also an object (if it exists),
when an object is passed where the key `named` is not present,
`this.args.named` will contain the result of the thunk and `this.args.positional`
will be empty.

#### An object containing both named args and positional args

when an object is passed containing the key `positional` or the value of `named` is an object:
when an object is passed containing either keys: `named` or `positional`:
- `this.args.named` will be the value of the result of the thunk's `named` property
- `this.args.positional` will be the value of the result of the thunk's `positional` property

Expand All @@ -78,7 +80,7 @@ when it needs to.

```ts
class MyClass {
myData = useTask(this.myTask, () => [args, to, task])
myData = useTask(this, this.myTask, () => [args, to, task])

@task
*myTask(args, to, task) { /* ... */ }
Expand Down Expand Up @@ -143,7 +145,7 @@ class MyResource extends LifecycleResource {
Using your custom Resource would look like
```ts
class ContainingClass {
data = useResource(MyResource, () => [this.ids])
data = useResource(this, MyResource, () => [this.ids])
}
```

Expand All @@ -156,7 +158,7 @@ invocation's return value as an argument to the next time the function is called
Example:
```ts
class StarWarsInfo {
info = useResource(async (state, ...args) => {
info = useResource(this, async (state, ...args) => {
if (state) {
let { characters } = state;

Expand Down Expand Up @@ -193,3 +195,13 @@ See the [Contributing](CONTRIBUTING.md) guide for details.
## License

This project is licensed under the [MIT License](LICENSE.md).


## Thanks

This library wouldn't be possible without the work of:
- [@pzuraq](https://github.com/pzuraq)
- [@josemarluedke](https://github.com/josemarluedke)

So much appreciate for the work both you have put in to Resources <3

17 changes: 8 additions & 9 deletions addon/-private/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,20 @@ import { associateDestroyableChild, registerDestructor } from '@ember/destroyabl
// @ts-ignore
import { capabilities as helperCapabilities, setHelperManager } from '@ember/helper';

import type { ArgsWrapper } from './types';
import type { ArgsWrapper, Cache } from './types';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Cache {
/* no clue what's in here */
export declare interface LifecycleResource<T extends ArgsWrapper> {
args: T;
setup(): void;
update(): void;
teardown(): void;
}

export class LifecycleResource<T extends ArgsWrapper> {
constructor(owner: unknown, protected args: T) {
constructor(owner: unknown, args: T) {
setOwner(this, owner);
this.args = args;
}

public declare setup: () => void;
public declare update: () => void;
public declare teardown: () => void;
}

class LifecycleResourceManager {
Expand Down
14 changes: 14 additions & 0 deletions addon/-private/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,17 @@ export interface ArgsWrapper {
positional?: unknown[];
named?: Record<string, unknown>;
}

// typed-ember should provide this from
// @glimmer/tracking/primitives/cache
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Cache<T = unknown> {
/* no clue what's in here */
_: T;
}

// typed-ember should provide this from @ember/helper
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Helper {
/* no clue what's in here */
}
93 changes: 92 additions & 1 deletion addon/-private/use-resource.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,94 @@
export function useResource() {
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-types */
// typed-ember has not publihsed types for this yet
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { getValue } from '@glimmer/tracking/primitives/cache';
// typed-ember has not publihsed types for this yet
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { invokeHelper } from '@ember/helper';

import type { ArgsWrapper, Cache } from './types';

interface Constructable<T> {
new (...args: unknown[]): T;
}

type Thunk =
// vanilla array
| (() => ArgsWrapper['positional'])
// plain named args
| (() => ArgsWrapper['named'])
// both named and positional args... but why would you choose this? :upsidedownface:
| (() => ArgsWrapper);

function normalizeThunk(thunk: Thunk): ArgsWrapper {
let args = thunk();

if (Array.isArray(args)) {
return { named: {}, positional: args };
}

if (!args) {
return { named: {}, positional: [] };
}

/**
* Hopefully people aren't using args named "named"
*/
if ('positional' in args || 'named' in args) {
return args;
}

return { named: args as Record<string, unknown>, positional: [] };
}

// https://github.com/josemarluedke/glimmer-apollo/blob/main/packages/glimmer-apollo/src/-private/use-resource.ts
function useUnproxiedResource<Instance = unknown>(
context: object,
klass: Constructable<Instance>,
thunk: Thunk
): { value: Instance } {
let resource: Cache<Instance>;

return {
get value(): Instance {
if (!resource) {
resource = invokeHelper(context, klass, () => {
return normalizeThunk(thunk);
}) as Cache<Instance>;
}

return getValue<Instance>(resource)!; // eslint-disable-line
},
};
}

/**
* For use in the body of a class.
* Constructs a cached Resource that will reactively respond to tracked data changes
*
*/
export function useResource<Instance extends object>(
context: object,
klass: Constructable<Instance>,
thunk: Thunk
): Instance {
const target = useUnproxiedResource<Instance>(context, klass, thunk);

return new Proxy(target, {
get(target, key): unknown {
const instance = target.value;
const value = Reflect.get(instance as object, key, instance);

return typeof value === 'function' ? value.bind(instance) : value;
},
ownKeys(target): (string | symbol)[] {
return Reflect.ownKeys(target.value);
},
getOwnPropertyDescriptor(target, key): PropertyDescriptor | undefined {
return Reflect.getOwnPropertyDescriptor(target.value, key);
},
}) as never as Instance;
}
Loading

0 comments on commit cef4396

Please sign in to comment.