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

Fix issue where, in some situations, the immediate-invoker helper manager (used when you use resourceFactory) was not correctly destroying the previous instance of a resource (such as when args change)) #1135

Merged
merged 6 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions ember-resources/src/function-based/immediate-invocation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-ignore
import { createCache, getValue } from '@glimmer/tracking/primitives/cache';
import { assert } from '@ember/debug';
import { associateDestroyableChild } from '@ember/destroyable';
import { associateDestroyableChild, destroy } from '@ember/destroyable';
// @ts-ignore
import { capabilities as helperCapabilities, invokeHelper, setHelperManager } from '@ember/helper';
import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros';
Expand All @@ -14,9 +14,6 @@ type ResourceFactory<Value = any, Args = any> = (...args: SpreadFor<Args>) => Va

interface State {
cache: ReturnType<typeof invokeHelper>;
fn: any;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were all unused

args: any;
_?: any;
}

let setOwner: (context: unknown, owner: Owner) => void;
Expand All @@ -40,6 +37,7 @@ class ResourceInvokerManager {
constructor(protected owner: Owner) {}

createHelper(fn: ResourceFactory, args: any): State {
let previous: object | undefined;
/**
* This cache is for args passed to the ResourceInvoker/Factory
*
Expand All @@ -51,12 +49,20 @@ class ResourceInvokerManager {

setOwner(resource, this.owner);

return invokeHelper(cache, resource);
let result = invokeHelper(cache, resource);

if (previous) {
destroy(previous);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fix

}

previous = result;

return result;
});

setOwner(cache, this.owner);

return { fn, args, cache, _: getValue(cache) };
return { cache };
}

/**
Expand All @@ -72,6 +78,9 @@ class ResourceInvokerManager {
}

getDestroyable({ cache }: State) {
/**
* This is the parent cache, from `createHelper`
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with nested caches, I had a hard time knowing which thing I was dealing with when, so I added some context

*/
return cache;
}
}
Expand Down
10 changes: 10 additions & 0 deletions ember-resources/src/function-based/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export function resource<Value>(
let internalConfig: InternalFunctionResourceConfig<Value> = {
definition: context as ResourceFunction<Value>,
type: 'function-based',
name: 'Resource',
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helps with debug output when errors from Glimmer print to the console

[INTERNAL]: true,
};

Expand Down Expand Up @@ -222,10 +223,19 @@ export function resource<Value>(
let internalConfig: InternalFunctionResourceConfig<Value> = {
definition: setup as ResourceFunction<Value>,
type: TYPE,
name: getDebugName(setup),
[INTERNAL]: true,
};

setHelperManager(ResourceManagerFactory, internalConfig);

return wrapForPlainUsage(context, internalConfig);
}

function getDebugName(obj: object) {
if ('name' in obj) {
return `Resource Function: ${obj.name}`;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helps with debug output when errors from Glimmer print to the console

}

return `Resource Function`;
}
1 change: 1 addition & 0 deletions ember-resources/src/function-based/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const INTERNAL = '__INTERNAL__';
export interface InternalFunctionResourceConfig<Value = unknown> {
definition: ResourceFunction<Value>;
type: 'function-based';
name: string;
[INTERNAL]: true;
}

Expand Down
14 changes: 10 additions & 4 deletions ember-resources/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": "@tsconfig/ember/tsconfig.json",
"include": ["src/**/*", "unpublished-development-types/**/*"],
"include": ["src/**/*"],
"glint": {
"environment": []
},
Expand Down Expand Up @@ -42,15 +42,21 @@
"allowImportingTsExtensions": true,

// require extensions
"module": "Node16",
"moduleResolution": "Node16",
// "module": "Node16",
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixes local dev experience

// "moduleResolution": "Node16",
// TODO: enable the above when this package gets to add type=module
"moduleResolution": "bundler",
"module": "esnext",
"target": "esnext",

// https://www.typescriptlang.org/tsconfig#stripInternal
"stripInternal": true,

// Build settings
"noEmitOnError": false,
// Just a good thing to do
"isolatedModules": true
"isolatedModules": true,

"types": ["ember-source/types"]
}
}
2 changes: 0 additions & 2 deletions ember-resources/unpublished-development-types/index.d.ts

This file was deleted.

5 changes: 5 additions & 0 deletions test-app/ember-cli-build.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
'use strict';

const path = require('path');
const EmberApp = require('ember-cli/lib/broccoli/ember-app');

const packageJson = require('./package');

module.exports = function (defaults) {
const sideWatch = require('@embroider/broccoli-side-watch');
let app = new EmberApp(defaults, {
trees: {
app: sideWatch('app', { watching: [path.join(__dirname, '../ember-resources')] }),
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allows me to not have to restart the app when I re-compile the addon (which is an injected dependency)

},
// Add options here
autoImport: {
watchDependencies: Object.keys(packageJson.dependencies),
Expand Down
1 change: 1 addition & 0 deletions test-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@ember/optional-features": "^2.0.0",
"@ember/string": "^3.0.1",
"@ember/test-helpers": "^3.2.1",
"@embroider/broccoli-side-watch": "0.0.2-unstable.ba9fd29",
"@embroider/compat": "^3.1.5",
"@embroider/core": "^3.1.3",
"@embroider/test-setup": "^3.0.1",
Expand Down
16 changes: 13 additions & 3 deletions test-app/pnpm-lock.yaml

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

64 changes: 64 additions & 0 deletions test-app/tests/core/issue-1134-test.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { clearRender,render, settled } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

import { cell, resource, resourceFactory } from 'ember-resources';


module('issues/1134', function(hooks) {
setupRenderingTest(hooks);

test('it works as a vanilla resource', async function (assert) {
let value = cell(0);

const Data = resource(({ on }) => {
let arg = value.current;

assert.step(`setup: ${arg}`);
on.cleanup(() => assert.step(`cleanup: ${arg}`));

return 0;
});

await render(<template>{{Data}}</template>);

assert.verifySteps(['setup: 0']);

value.current = 1;
await settled();
assert.verifySteps(['setup: 1', 'cleanup: 0']);

await clearRender();
assert.verifySteps([`cleanup: ${value.current}`]);

});

test('it works with a Wrapper (resourceFactory)', async function (assert) {
let value = cell(0);

function Wrapper(arg: number) {
assert.step(`wrapper: ${arg}`);

return resource(({ on }) => {
assert.step(`setup: ${arg}`);
on.cleanup(() => assert.step(`cleanup: ${arg}`));

return 0;
});
}

resourceFactory(Wrapper);

await render(<template>{{Wrapper value.current}}</template>);

assert.verifySteps(['wrapper: 0', 'setup: 0']);

value.current = 1;
await settled();
assert.verifySteps(['wrapper: 1', 'setup: 1', 'cleanup: 0']);

await clearRender();
assert.verifySteps([`cleanup: ${value.current}`]);
});

});
Loading