Skip to content

Commit

Permalink
Merge pull request #960 from NullVoxPopuli/provide-the-ability-to-imm…
Browse files Browse the repository at this point in the history
…ediately-return-used-resources

Resolves #958
  • Loading branch information
NullVoxPopuli authored Jul 26, 2023
2 parents 3002ec8 + cc363c2 commit 3f19818
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 81 deletions.
23 changes: 23 additions & 0 deletions .changeset/sour-pears-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"ember-resources": patch
---

Resolves: https://github.com/NullVoxPopuli/ember-resources/issues/958

`use`d Resources can now be immediately returned from other resources.

```js
const Clock = resource(({ use }) => {
return use(Instant({ intervalMs: 1000 });
});

const Stopwatch = resource(({ use }) => {
return use(Instant({ intervalMs: 0 });
});

<template>
<time>{{Clock}}</time>

MS since Epoch: {{Stopwatch}}
</template>
```
14 changes: 0 additions & 14 deletions ember-resources/src/core/class-based/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,6 @@ import { Resource } from './resource';
import type { ArgsWrapper } from '[core-types]';
import type Owner from '@ember/owner';

/**
*
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
// export interface Resource<T extends ArgsWrapper = ArgsWrapper> extends InstanceType<
// HelperLike<{
// Args: {
// Named: NonNullable<T['named']>;
// Positional: NonNullable<T['positional']>
// };
// // Return: number
// }>
// > {}

class ResourceManager {
capabilities = helperCapabilities('3.23', {
hasValue: true,
Expand Down
33 changes: 20 additions & 13 deletions ember-resources/src/core/function-based/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { invokeHelper } from '@ember/helper';
import { capabilities as helperCapabilities } from '@ember/helper';
import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros';

import { Cell } from '../../util/cell';
import { INTERNAL } from './types';
import { ReadonlyCell } from '../../util/cell';
import { CURRENT, INTERNAL } from './types';

import type {
Cache,
Expand All @@ -22,15 +22,18 @@ import type { ResourceAPI } from './types';
import type Owner from '@ember/owner';

let getOwner: (context: unknown) => Owner | undefined;
let setOwner: (context: unknown, owner: Owner) => void;

if (macroCondition(dependencySatisfies('ember-source', '>=4.12.0'))) {
// In no version of ember where `@ember/owner` tried to be imported did it exist
// if (macroCondition(false)) {
// Using 'any' here because importSync can't lookup types correctly
getOwner = (importSync('@ember/owner') as any).getOwner;
setOwner = (importSync('@ember/owner') as any).setOwner;
} else {
// Using 'any' here because importSync can't lookup types correctly
getOwner = (importSync('@ember/application') as any).getOwner;
setOwner = (importSync('@ember/application') as any).setOwner;
}

/**
Expand Down Expand Up @@ -93,19 +96,17 @@ class FunctionResourceManager {
destroy(previousCache);
}

let cache = invokeHelper(owner, usable);
let nestedCache = invokeHelper(cache, usable);

associateDestroyableChild(currentFn, cache as object);
associateDestroyableChild(currentFn, nestedCache as object);

usableCache.set(usable, cache);
usableCache.set(usable, nestedCache);

return {
get current() {
let cache = usableCache.get(usable);
return new ReadonlyCell(() => {
let cache = usableCache.get(usable);

return getValue(cache);
},
};
return getValue(cache);
});
};

let maybeValue = currentFn({
Expand All @@ -121,6 +122,8 @@ class FunctionResourceManager {
return maybeValue;
});

setOwner(cache, owner);

return { fn: thisFn, cache };
}

Expand All @@ -131,8 +134,8 @@ class FunctionResourceManager {
return maybeValue();
}

if (maybeValue instanceof Cell) {
return maybeValue.current;
if (isReactive(maybeValue)) {
return maybeValue[CURRENT];
}

return maybeValue;
Expand All @@ -143,4 +146,8 @@ class FunctionResourceManager {
}
}

function isReactive<Value>(maybe: unknown): maybe is Reactive<Value> {
return typeof maybe === 'object' && maybe !== null && CURRENT in maybe;
}

export const ResourceManagerFactory = (owner: Owner) => new FunctionResourceManager(owner);
21 changes: 20 additions & 1 deletion ember-resources/src/core/function-based/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,28 @@ export interface InternalFunctionResourceConfig<Value = unknown> {
[INTERNAL]: true;
}

export const CURRENT = Symbol('ember-resources::CURRENT');

export interface GlintRenderable {
/**
* Cells aren't inherently understood by Glint,
* so to work around that, we'll hook in to the fact that
* ContentValue (the type expected for all renderables),
* defines an interface with this signature.
*
* (SafeString)
*
* There *has* been interest in the community to formally support
* toString and toHTML APIs across all objects. An RFC needs to be
* written so that we can gather feedback / potential problems.
*/
toHTML(): string;
}

// Will need to be a class for .current flattening / auto-rendering
export interface Reactive<Value> {
export interface Reactive<Value> extends GlintRenderable {
current: Value;
[CURRENT]: Value;
[Invoke]?: Value;
}

Expand Down
19 changes: 9 additions & 10 deletions ember-resources/src/core/use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { associateDestroyableChild } from '@ember/destroyable';
// @ts-ignore
import { invokeHelper } from '@ember/helper';

import { ReadonlyCell } from '../util/cell';
import { INTERNAL } from './function-based/types';
import { normalizeThunk } from './utils';

Expand Down Expand Up @@ -120,19 +121,17 @@ function classContextLink<Value>(
): Reactive<Value> {
let cache: ReturnType<typeof invokeHelper>;

return {
get current() {
if (!cache) {
cache = invokeHelper(context, definition);
return new ReadonlyCell<Value>(() => {
if (!cache) {
cache = invokeHelper(context, definition);

associateDestroyableChild(context, cache);
}
associateDestroyableChild(context, cache);
}

let value = getValue(cache);
let value = getValue(cache);

return getCurrentValue(value);
},
};
return getCurrentValue(value);
});
}

function argumentToDecorator<Value>(definition: Value | (() => Value)): PropertyDecorator {
Expand Down
44 changes: 29 additions & 15 deletions ember-resources/src/util/cell.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
import { tracked } from '@glimmer/tracking';
import { assert } from '@ember/debug';

interface GlintRenderable {
/**
* Cells aren't inherently understood by Glint,
* so to work around that, we'll hook in to the fact that
* ContentValue (the type expected for all renderables),
* defines an interface with this signature.
*
* (SafeString)
*
* There *has* been interest in the community to formally support
* toString and toHTML APIs across all objects. An RFC needs to be
* written so that we can gather feedback / potential problems.
*/
toHTML(): string;
export class ReadonlyCell<Value> implements Reactive<Value> {
#getter: () => Value;

constructor(getter: () => Value) {
this.#getter = getter;
}

toHTML(): string {
assert(
'Not a valid API. Please access either .current or .read() if the value of this Cell is needed'
);
}

get [CURRENT](): Value {
return this.current;
}

get current(): Value {
return this.#getter();
}
}

export class Cell<Value = unknown> implements GlintRenderable {
export class Cell<Value = unknown> implements Reactive<Value> {
@tracked declare current: Value;

get [CURRENT](): Value {
return this.current;
}

toHTML(): string {
assert(
'Not a valid API. Please access either .current or .read() if the value of this Cell is needed'
Expand Down Expand Up @@ -132,6 +142,10 @@ export function cell<Value = unknown>(initialValue?: Value): Cell<Value> {
// @ts-ignore
import { capabilities as helperCapabilities, setHelperManager } from '@ember/helper';

import { CURRENT } from '../core/function-based/types';

import type { GlintRenderable, Reactive } from '../core/function-based/types';

class CellManager {
capabilities = helperCapabilities('3.23', {
hasValue: true,
Expand Down
Loading

0 comments on commit 3f19818

Please sign in to comment.