Skip to content

Commit 1879cc9

Browse files
fix(effects): register functional effects from object without prototype (#3984)
Closes #3972
1 parent caa74ff commit 1879cc9

File tree

5 files changed

+51
-7
lines changed

5 files changed

+51
-7
lines changed

modules/effects/spec/effect_sources.spec.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ describe('EffectSources', () => {
171171
this.effectIdentifier = identifier;
172172
}
173173
}
174+
174175
class SourceWithInitAction implements OnInitEffects, OnIdentifyEffects {
175176
effectIdentifier: string;
176177

@@ -207,6 +208,17 @@ describe('EffectSources', () => {
207208
const recordB = {
208209
b: createEffect(() => alwaysOf(b), { functional: true }),
209210
};
211+
// a record with functional effects that is defined as
212+
// a named import in a built package doesn't have a prototype
213+
// for more info see: https://github.com/ngrx/platform/issues/3972
214+
const recordC = Object.freeze({
215+
__proto__: null,
216+
c: createEffect(() => alwaysOf(c), { functional: true }),
217+
});
218+
const recordD = Object.freeze({
219+
__proto__: null,
220+
d: createEffect(() => alwaysOf(d as any), { functional: true }),
221+
});
210222

211223
it('should resolve effects from class instances', () => {
212224
const sources$ = cold('--a--b--', {
@@ -221,8 +233,12 @@ describe('EffectSources', () => {
221233
});
222234

223235
it('should resolve effects from records', () => {
224-
const sources$ = cold('--a--b--', { a: recordA, b: recordB });
225-
const expected = cold('--a--b--', { a, b });
236+
const sources$ = cold('--a--b--c--', {
237+
a: recordA,
238+
b: recordB,
239+
c: recordC,
240+
});
241+
const expected = cold('--a--b--c--', { a, b, c });
226242

227243
const output = toActions(sources$);
228244

@@ -338,7 +354,7 @@ describe('EffectSources', () => {
338354
expect(output).toBeObservable(expected);
339355
});
340356

341-
it('should report an error if an effect dispatches an invalid action', () => {
357+
it('should report an error if a class-based effect dispatches an invalid action', () => {
342358
const sources$ = of(new SourceD());
343359

344360
toActions(sources$).subscribe();
@@ -350,6 +366,18 @@ describe('EffectSources', () => {
350366
);
351367
});
352368

369+
it('should report an error if a functional effect dispatches an invalid action', () => {
370+
const sources$ = of(recordD);
371+
372+
toActions(sources$).subscribe();
373+
374+
expect(mockErrorReporter.handleError).toHaveBeenCalledWith(
375+
new Error(
376+
'Effect "d()" dispatched an invalid action: {"not":"a valid action"}'
377+
)
378+
);
379+
});
380+
353381
it('should report an error if an effect dispatches an `undefined`', () => {
354382
const sources$ = of(new SourceE());
355383

modules/effects/spec/utils.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ describe('isClassInstance', () => {
3535
it('returns false for a function', () => {
3636
expect(isClassInstance(() => {})).toBe(false);
3737
});
38+
39+
it('returns false for an object without prototype', () => {
40+
const obj = Object.freeze({
41+
__proto__: null,
42+
foo: 'bar',
43+
});
44+
45+
expect(isClassInstance(obj)).toBe(false);
46+
});
3847
});
3948

4049
describe('isClass', () => {

modules/effects/src/effect_notification.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ObservableNotification } from './utils';
66
export interface EffectNotification {
77
effect: Observable<any> | (() => Observable<any>);
88
propertyName: PropertyKey;
9-
sourceName: string;
9+
sourceName: string | null;
1010
sourceInstance: any;
1111
notification: ObservableNotification<Action | null | undefined>;
1212
}
@@ -46,8 +46,11 @@ function getEffectName({
4646
sourceName,
4747
}: EffectNotification) {
4848
const isMethod = typeof sourceInstance[propertyName] === 'function';
49+
const isClassBasedEffect = !!sourceName;
4950

50-
return `"${sourceName}.${String(propertyName)}${isMethod ? '()' : ''}"`;
51+
return isClassBasedEffect
52+
? `"${sourceName}.${String(propertyName)}${isMethod ? '()' : ''}"`
53+
: `"${String(propertyName)}()"`;
5154
}
5255

5356
function stringify(action: Action | null | undefined) {

modules/effects/src/effects_resolver.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export function mergeEffects(
1313
globalErrorHandler: ErrorHandler,
1414
effectsErrorHandler: EffectsErrorHandler
1515
): Observable<EffectNotification> {
16-
const sourceName = getSourceForInstance(sourceInstance).constructor.name;
16+
const source = getSourceForInstance(sourceInstance);
17+
const isClassBasedEffect = !!source && source.constructor.name !== 'Object';
18+
const sourceName = isClassBasedEffect ? source.constructor.name : null;
1719

1820
const observables$: Observable<any>[] = getSourceMetadata(sourceInstance).map(
1921
({

modules/effects/src/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ export function getSourceForInstance<T>(instance: T): T {
66

77
export function isClassInstance(obj: object): boolean {
88
return (
9-
obj.constructor.name !== 'Object' && obj.constructor.name !== 'Function'
9+
!!obj.constructor &&
10+
obj.constructor.name !== 'Object' &&
11+
obj.constructor.name !== 'Function'
1012
);
1113
}
1214

0 commit comments

Comments
 (0)